Last active
May 11, 2024 16:05
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Demo:
demo.mp4