10

Does anybody have a simple example on how to call a C function from Java 17 that includes creating a C library and how to setup a MethodHandle?

The JEP description at https://openjdk.java.net/jeps/412 does have an example, but I've been struggling to understand it.

I think it can be achieved using Project Panama (https://jdk.java.net/panama/) jextract, but since this feature was included in the JDK, I didn't want to use Panama.

bric3
  • 40,072
  • 9
  • 91
  • 111
  • Your question is too general. Investigate Java + C issues separately, and add detail on what you done. I will post an answer which may help you get started. – DuncG Sep 27 '21 at 13:15
  • You might find useful this fresh video, for the 3rd preview of [*Foreign Function & Memory API*](https://openjdk.org/jeps/442) in Java 21: [*Java Applications Meet Native Libraries: Testing Preview Features with Custom Code*](https://youtu.be/KsZ8ICRfib0) by Daniel Persson. He shows first a simple call to `strlen` from Java. Then he goes on to write a bit of C code, compile it, and call it from Java. – Basil Bourque Aug 22 '23 at 21:32

1 Answers1

14

I collated various of the code snippets I read from JEP412 / Panama into this simple example class, which demonstrates how to set up method handles in Java 17 for calling C runtime library and showing use of Java->C and C->Java calls.

It is not optimised in any way. Longer term using jextract is much better than handcoding the various method handles for up/downcalls from Java->C and C->Java, and it will hugely cut down on the effort to build Java API mappings for your own or other native library.

The sample will run on JDK17 Windows 10 and GNU/Linux (I used OpenJDK 17), though for GNU/Linux you may need to adjust setting for library loads such as -Djava.library.path=/lib/x86_64-linux-gnu.

Run the example with command:

java --enable-native-access=ALL-UNNAMED --add-modules jdk.incubator.foreign Example17.java

Note the Foreign Memory API is in incubating stage, so APIs change frequently. With small extra effort this sample could be adjusted run on JDK16 or the latest build of JDK Panama Foreign. The C runtime calls are built-in but if you experiment with other libraries you would need to call System.loadLibrary("xyz"); before symbol lookups from xyz.dll or libxyz.so.

import static jdk.incubator.foreign.CLinker.*;
import java.lang.invoke.*;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Consumer;
import jdk.incubator.foreign.*;
import jdk.incubator.foreign.CLinker.VaList.Builder;

/**
 * Example calls to C library methods from Java/JDK17. 
 * Run on Windows with:
 java --enable-native-access=ALL-UNNAMED --add-modules jdk.incubator.foreign Example17.java
 * Run on GNU/Linux with:
 java -Djava.library.path=/lib/x86_64-linux-gnu --enable-native-access=ALL-UNNAMED --add-modules jdk.incubator.foreign Example17.java
*/
public class Example17 {

    private static final CLinker CLINKER = CLinker.getInstance();
    // LOADER is used for symbols from xyz.dll or libxyz.so
    // For every external library dependency add: System.loadLibrary("xyz");
    private static final SymbolLookup LOADER = SymbolLookup.loaderLookup();
    // SYSTEM is used for built-in C runtime library calls.
    private static final SymbolLookup SYSTEM = CLinker.systemLookup();

    static {
        System.out.println("os.name="+System.getProperty("os.name"));
        System.out.println("java.library.path="
                +String.join(System.lineSeparator()+"\t", System.getProperty("java.library.path").split(File.pathSeparator)));
    }
    /** Find native symbol, call System.loadLibrary("xyz") for each dependency */
    static MemoryAddress lookup(String name) {
        return Objects.requireNonNull(LOADER.lookup(name).or(() -> SYSTEM.lookup(name)).get(), () -> "Not found native method: "+name);
    }

    /** Example calls to C runtime library */
    public static void main(String... args) throws Throwable {

        getpid();

        strlen("Hello World");

        printf();

        qsort(0, 9, 33, 45, 3, 4, 6, 5, 1, 8, 2, 7);

        vprintf("ONE=%d\n", 1234);
        vprintf("A=%d B=%d\n", 2, 4);
        vprintf("%d plus %d equals %d\n", 5, 7, 12);
    }

    // get a native method handle for 'getpid' function
    private static final MethodHandle GETPID$MH = CLINKER.downcallHandle(
                    lookup(System.getProperty("os.name").startsWith("Windows") ? "_getpid":"getpid"),
                    MethodType.methodType(int.class),
                    FunctionDescriptor.of(CLinker.C_INT));

    private static void getpid() throws Throwable {
        int npid = (int)GETPID$MH.invokeExact();
        System.out.println("getpid() JAVA => "+ProcessHandle.current().pid()+" NATIVE => "+npid);
    }

    private static final MethodHandle STRLEN$MH = CLINKER.downcallHandle(lookup("strlen"),
            MethodType.methodType(long.class, MemoryAddress.class), FunctionDescriptor.of(C_LONG_LONG, C_POINTER));
    public static void strlen(String s) throws Throwable {
        System.out.println("strlen('"+s+"')");

        // size_t strlen(const char *str);
        try(ResourceScope scope = ResourceScope.newConfinedScope()) {
            SegmentAllocator allocator = SegmentAllocator.arenaAllocator(scope);
            MemorySegment hello = CLinker.toCString(s, allocator);
            long len = (long) STRLEN$MH.invokeExact(hello.address()); // 5
            System.out.println(" => "+len);
        }
    }

    static class Qsort {
        static int qsortCompare(MemoryAddress addr1, MemoryAddress addr2) {
            int v1 = MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(), addr1.toRawLongValue());
            int v2 = MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(), addr2.toRawLongValue());
            return v1 - v2;
        }
    }

    private static final MethodHandle QSORT$MH = CLINKER.downcallHandle(lookup("qsort"),
            MethodType.methodType(void.class, MemoryAddress.class, long.class, long.class, MemoryAddress.class),
            FunctionDescriptor.ofVoid(C_POINTER, C_LONG_LONG, C_LONG_LONG, C_POINTER)
    );

    /**
     * THIS SHOWS DOWNCALL AND UPCALL - uses qsortCompare FROM C code!
     * void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
     * @param toSort
     */
    public static int[] qsort(int ... toSort) throws Throwable {
        System.out.println("qsort() "+Arrays.toString(toSort));

        MethodHandle comparHandle = MethodHandles.lookup()
                .findStatic(Qsort.class, "qsortCompare",
                        MethodType.methodType(int.class, MemoryAddress.class, MemoryAddress.class));

        try(ResourceScope scope = ResourceScope.newConfinedScope()) {
            MemoryAddress comparFunc = CLINKER.upcallStub(
                    comparHandle,FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER),scope
            );

            SegmentAllocator allocator = SegmentAllocator.arenaAllocator(scope);
            // comparFunc = allocator.register(comparFunc);
            MemorySegment array = allocator.allocateArray(CLinker.C_INT, toSort);
            QSORT$MH.invokeExact(array.address(), (long)toSort.length, 4L, comparFunc.address());
            int[] sorted = array.toIntArray();
            System.out.println(" => "+Arrays.toString(sorted));
            return sorted;
        }
    }

    private static final MethodHandle PRINTF$MH = CLINKER.downcallHandle(lookup("printf"),
            MethodType.methodType(int.class, MemoryAddress.class, int.class, int.class, int.class),
            FunctionDescriptor.of(C_INT,    C_POINTER,           C_INT,    C_INT,    C_INT)
    );
    /** This version hard-codes use of 3 int params as args to the string format */
    public static void printf() throws Throwable {
        System.out.println("printf()");
        int a = 10;
        int b = 7;
        try(ResourceScope scope = ResourceScope.newConfinedScope()) {
            SegmentAllocator allocator = SegmentAllocator.arenaAllocator(scope);
            MemorySegment s = CLinker.toCString("%d times %d equals %d\n", allocator);
            int rc = (int)PRINTF$MH.invokeExact(s.address(), a, b, a * b);
            System.out.println(" => rc="+rc);
        }
    }

    private static final MethodHandle vprintf = CLINKER.downcallHandle(lookup("vprintf"),
            MethodType.methodType(int.class, MemoryAddress.class, CLinker.VaList.class),
            FunctionDescriptor.of(C_INT,     C_POINTER,           C_VA_LIST));

    /**
     * vprintf takes a pointer to arg list rather than arg list as for printf
     */
    public static void vprintf(String format, int ... args) throws Throwable {

        System.out.println("vprintf(\""+format.replaceAll("[\r\n]{1,2}", "\\\\n")+"\") "+Arrays.toString(args));

        // Weird Builder callback mechanism to fill the varargs values
        Consumer<Builder> actions = builder -> {
             for (int v : args) builder.vargFromInt(CLinker.C_INT, v);
        };

        try(ResourceScope scope = ResourceScope.newConfinedScope()) {
            SegmentAllocator allocator = SegmentAllocator.arenaAllocator(scope);
            CLinker.VaList vlist = CLinker.VaList.make(actions, scope);
            MemorySegment s = CLinker.toCString(format, allocator);

            int rc = (int)vprintf.invokeExact(s.address(), vlist /* ????? .address() */);
            System.out.println(" => rc="+rc);
        }
    }    }
DuncG
  • 12,137
  • 2
  • 21
  • 33
  • Thank you @DuncG for this answer. I was able to invoke function from my own DLL using this. – Yogesh Chaudhari Sep 30 '21 at 19:04
  • Thanks. It looks very verbose, even for Java. Still, if it's stable and if it allows native code to be called without modifying the corresponding source, it's a win. From the description (https://openjdk.java.net/jeps/412), I don't understand if it already supports C++/Fortran, or only "over time". – Eric Duminil Dec 04 '21 at 14:55
  • 2
    @Eric Duminil The `jextract` tool is incredibly easy to use and generates the bindings for C header files, and it greatly shortens and simplies the work needed to get native library calls working. I've now got my Java apps using Windows APIs / OLE directly in 100% Java. However `jextract` isn't in JDK17, so for the above example you see all the hand crafted MethodHandle declarations for foreign API calls. I don't believe C++/Fortran are supported yet. – DuncG Dec 04 '21 at 15:45
  • Thanks. The example at https://inside.java/2020/10/06/jextract/ looks good. Do you know if the new 100% Java API delivers more informative error message when something goes wrong, as opposed to "SIGSEGV somewhere else. Not in JVM, not my problem"? – Eric Duminil Dec 04 '21 at 15:57
  • If things go wrong inside the native code you should hopefully see a `hs_err_pid` log file with native frame + java frame info. – DuncG Dec 04 '21 at 16:33
  • I don't know enough in order to understand what's written inside the hs_err file (which is indeed written). It's often just "SIGSEGV" in libc, with the program I work on, and no other stack info. The crashes seem kinda random, and the java frame info is different each time. :-/ – Eric Duminil Dec 04 '21 at 16:38
  • If the stack frames are no help you should be able to attach debugger to the JVM process - just as you would do for testing non-JVM executables - and assuming there is source code / debug symbol info for your native code. – DuncG Dec 04 '21 at 17:00
  • 1
    When I run this in Java 17, I get WARNING: Using incubator modules: jdk.incubator.foreign warning: using incubating module(s): jdk.incubator.foreign Example17.java:24: error: cannot find symbol final boolean isWindows = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows"); symbol: variable Locale location: class Example17 1 error 1 warning error: compilation failed – hobgoblin Mar 09 '22 at 20:08
  • The `java.util.Locale` class is pretty standard so I left it out (as per SO guidelines), just add an extra `import`. – DuncG Mar 09 '22 at 20:27
  • Has anyone here profiled FFM API code for object allocations? If I'm not mistaken, `VarHandle` and `MethodHandle` get optimized away, but I'm worried about all these other constructions and lookups that occur. Hard to move away from JNI and `sun.misc.Unsafe` for real-time use if there's any GC impact here... – Philip Guin Jun 11 '22 at 04:39