Skip to content

Instantly share code, notes, and snippets.

@plamentotev
Last active May 11, 2024 16:05
Show Gist options
  • Save plamentotev/fff031eca3722df792db86096e238d02 to your computer and use it in GitHub Desktop.
Save plamentotev/fff031eca3722df792db86096e238d02 to your computer and use it in GitHub Desktop.
Sample application that uses JEP 454: Foreign Function & Memory API to create simple text UI with ncurses. Demo: https://gist.github.com/plamentotev/fff031eca3722df792db86096e238d02?permalink_comment_id=5043190#gistcomment-5043190
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class HelloWorld {
public static void main(String[] args) throws Throwable {
// Arena handles the lifecycle of native memory segments.
// There are different implementations, the confined one would
// free the memory after the try-with-resources completes
try(var arena = Arena.ofConfined()) {
// See below for the implementation, but NCurses is utility
// class initialized with MethodHandle instances for the
// ncurses functions we need.
var ncurses = new NCurses(arena);
// Once we have MethodHandle for foreign function, we can call by invoking `invoke`.
// The return value is the return value of the function.
// In this case a pointer to the main window.
var mainWindow = ncurses.initscr.invoke();
ncurses.noecho.invoke();
// Remember `mainWindow` from above? `wprintw` accepts pointer to a window
// (`mainWindow` in this case) and pointer to a string.
// Allocating array of chars and passing the pointer is trivial - just call
// `Arena#allocateFrom` and it will allocate it on the native memory segment
// and return a pointer you can pass to the `invoke` method.
ncurses.wprintw.invoke(mainWindow, arena.allocateFrom("Press "));
ncurses.wattron.invoke(mainWindow, NCurses.A_STANDOUT);
ncurses.wprintw.invoke(mainWindow, arena.allocateFrom("q"));
ncurses.wattroff.invoke(mainWindow, NCurses.A_STANDOUT);
ncurses.wprintw.invoke(mainWindow, arena.allocateFrom(" to quit"));
ncurses.wrefresh.invoke(mainWindow);
// If the foreign function accepts int, double, etc. you can just pass the
// primitive value, no need to do anything special for those.
var msgWindow = ncurses.newwin.invoke(5, 25, 5, 30);
ncurses.box.invoke(msgWindow, 0, 0);
ncurses.wmove.invoke(msgWindow, 2, 6);
ncurses.wprintw.invoke(msgWindow, arena.allocateFrom("Hello, "));
ncurses.wattron.invoke(msgWindow, NCurses.A_UNDERLINE);
ncurses.wprintw.invoke(msgWindow, arena.allocateFrom("World"));
ncurses.wattroff.invoke(msgWindow, NCurses.A_UNDERLINE);
ncurses.wprintw.invoke(msgWindow, arena.allocateFrom("!"));
ncurses.wrefresh.invoke(msgWindow);
ncurses.wmove.invoke(mainWindow, 0, 15);
int ch;
do {
ch = (int) ncurses.getch.invoke();
} while (ch != 'q');
ncurses.delwin.invoke(mainWindow);
ncurses.endwin.invoke();
}
}
}
final class NCurses {
// We'll see how those are used below
private final SymbolLookup ncurses;
private final Linker linker;
// Some ncurses related constants. Unfortunately there is no way to get those
// form the native library.
static final int A_STANDOUT = 65536;
static final int A_UNDERLINE = 131072;
// MethodHandle for the functions that we need for the demo
final MethodHandle initscr;
final MethodHandle wprintw;
final MethodHandle wrefresh;
final MethodHandle getch;
final MethodHandle endwin;
final MethodHandle noecho;
final MethodHandle wattron;
final MethodHandle wattroff;
final MethodHandle newwin;
final MethodHandle delwin;
final MethodHandle box;
final MethodHandle wmove;
NCurses(Arena arena) {
// This is where to look for the ncurses symbols.
// Unfortunately the library name is platform dependant:
// calling `libraryLookup` with only `libncurses` would fail.
// And on Windows obviously the library has `dll` extension,
// so calling with `libncurses.so.6` would fail on Windows.
ncurses = SymbolLookup.libraryLookup("libncurses.so.6", arena);
linker = Linker.nativeLinker();
// FunctionDescriptor describes the function signature. With the function name
// and the descriptor we can acquire MethodHandle for it. Which in turn is used to call the function.
// See `initFunction` below.
// Notice that the first parameter for `FunctionDescriptor.of` is the return type,
// the rest are the function arguments.
initscr = initFunction("initscr", FunctionDescriptor.of(ValueLayout.ADDRESS));
wprintw = initFunction("wprintw", FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS, ValueLayout.ADDRESS));
wrefresh = initFunction("wrefresh", FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS));
getch = initFunction("getch", FunctionDescriptor.of(ValueLayout.JAVA_INT));
endwin = initFunction("endwin", FunctionDescriptor.of(ValueLayout.JAVA_INT));
noecho = initFunction("noecho", FunctionDescriptor.of(ValueLayout.JAVA_INT));
wattron = initFunction("wattron", FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS, ValueLayout.JAVA_INT));
wattroff = initFunction("wattroff", FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS, ValueLayout.JAVA_INT));
newwin = initFunction("newwin", FunctionDescriptor.of(ValueLayout.ADDRESS,
ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));
delwin = initFunction("attroff", FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS));
box = initFunction("box", FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));
wmove = initFunction("wmove", FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS, ValueLayout.JAVA_INT, ValueLayout.JAVA_INT));
}
// To acquire MethodHandle for foreign function all we need is the name and the descriptor (the function signature)
private MethodHandle initFunction(String functionName, FunctionDescriptor descriptor) {
var functionAddress = ncurses.find(functionName).orElseThrow();
return linker.downcallHandle(functionAddress, descriptor);
}
}
@plamentotev
Copy link
Author

plamentotev commented May 1, 2024

Demo:

demo.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment