Skip to content

Foreign Function Interface

Karstian Lee edited this page Dec 31, 2023 · 1 revision

Handle Mapping

UXGL provides a set of simple and clear API ("UXGL FFI") to easily integrate native code. Let's begin with the basis, UXGL "Handle Mapping"!.

package org.example.desktop.foreign;

import unrefined.app.Logger;
import unrefined.runtime.DesktopRuntime;
import unrefined.util.foreign.Foreign;
import unrefined.util.foreign.Symbol;

public class ForeignFunction {

    public static void main(String[] args) throws NoSuchMethodException {
        DesktopRuntime.setup(args);             // Initialize the UXGL runtime environment

        // Type mapping:
        // Java void -> void
        // Java boolean -> uint8_t
        // Java byte -> int8_t
        // Java char -> uint16_t
        // Java short -> int16_t
        // Java int -> int32_t
        // Java long -> int64_t
        // Java float -> float
        // Java double -> double
        // Java unrefined.nio.Pointer -> pointer
        // Note that unrefined.nio.Pointer is used just as a "marker class",
        // for pointer types, the method parameter type in Java side should be long.class

        // Varargs supported for the downcall handle, but the upcall stub not

        Foreign foreign = Foreign.getInstance(); // Get the platform-dependent FFI factory
        Symbol upcallStub = foreign.upcallStub(ForeignFunction.class.getDeclaredMethod("helloFFI"),
                void.class); // Create a upcall stub, which a closure/callback will call Java method from a corresponding C function pointer
        Symbol downcallHandle = foreign.downcallHandle(upcallStub.address(), void.class); // Create a downcall handle, which calls C function from Java
        downcallHandle.invokeVoid(); // Java -> downcall -> C -> upcall -> Java

        //upcallStub.invokeVoid();     // In fact, you can directly invoke the upcall stub without create a new downcall handle
    }

    public static void helloFFI() {
        Logger.defaultInstance().info("UXGL FFI", "C call Java from C from Java");
    }

}

Direct Mapping

With some limitations, you can dynamically call some native functions as fast as hand-writing JNI. (Okay, a bit slower than JNI indeed) Works similar with JNA Direct Mapping, that's why I named it UXGL "Direct Mapping"!

package org.example.desktop.foreign;

import unrefined.app.Logger;
import unrefined.runtime.DesktopRuntime;
import unrefined.util.foreign.Foreign;

public class FastForeignCall {

    public static void main(String[] args) {
        DesktopRuntime.setup(args);             // Initialize the UXGL runtime environment

        Foreign foreign = Foreign.getInstance(); // Get the platform-dependent FFI factory
        foreign.register(FastForeignCall.class); // Register all the native functions in specified class

        final double x = 1;

        long time;

        time = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i ++) {
            Math.sin(x);
        }
        time = System.currentTimeMillis() - time;
        Logger.defaultInstance().info("UXGL FFI", "java.lang.Math#sin (10m call) time: " + time);
        time = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i ++) {
            sin(x);
        }
        time = System.currentTimeMillis() - time;
        Logger.defaultInstance().info("UXGL FFI", "<cmath> sin (UXGL FFI Direct Mapping, 10m call) time: " + time);
    }

    // Only Java primitive types allowed! The pointer type and varargs are unsupported!
    // Java void -> void
    // Java boolean -> uint8_t
    // Java byte -> int8_t
    // Java char -> uint16_t
    // Java short -> int16_t
    // Java int -> int32_t
    // Java long -> int64_t
    // Java float -> float
    // Java double -> double
    public static native double sin(double x);

}

Library Mapping

UXGL "Library Mapping"! Dynamic proxy objects, working just like JNA! Note that type mapping as the same of UXGL "Handle Mapping", different from JNA.

package org.example.desktop.foreign;

import unrefined.nio.Pointer;
import unrefined.runtime.DesktopRuntime;
import unrefined.util.UnexpectedError;
import unrefined.util.foreign.Foreign;
import unrefined.util.foreign.Library;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class LibraryMapping {

    public interface CLibrary extends Library {
        void printf(long format, int... args); // Varargs supported!
    }

    public static void main(String[] args) {
        DesktopRuntime.setup(args);              // Initialize the UXGL runtime environment
        Foreign foreign = Foreign.getInstance(); // Get the platform-dependent FFI factory

        Random random = ThreadLocalRandom.current();

        int a = random.nextInt();
        int b = random.nextInt();

        CLibrary c = foreign.downcallProxy(CLibrary.class);
        try (Pointer format = Pointer.allocateDirect("SUM (%d, %d) = %d")) {
            c.printf(format.address(), a, b, a + b);
        }
        catch (IOException e) {
            throw new UnexpectedError(e);
        }
    }

}

Symbol Management

Symbols are internally managed by UXGL. Load the library, get the symbol, and do whatever you want!

package org.example.desktop.foreign;

import unrefined.app.Logger;
import unrefined.runtime.DesktopRuntime;
import unrefined.util.StringCompat;
import unrefined.util.foreign.Foreign;
import unrefined.util.foreign.Redirect;
import unrefined.util.foreign.Symbol;

import java.io.IOException;

public class SymbolManagement {

    public static final boolean IS_WINDOWS = StringCompat.stripLeading(System.getProperty("os.name")).startsWith("Windows");
    public static final boolean IS_LINUX = System.getProperty("os.name").equalsIgnoreCase("linux");

    public static void main(String[] args) throws IOException {
        DesktopRuntime.setup(args);              // Initialize the UXGL runtime environment
        Foreign foreign = Foreign.getInstance(); // Get the platform-dependent FFI factory

        foreign.loadLibrary(foreign.mapLibraryName(IS_WINDOWS ? "Kernel32" : (IS_LINUX ? "libc.so.6" : "c")),
                Foreign.Loader.LINKER); // Load the library for symbols we want.
        // The symbols whether ClassLoader-managed or linker-managed depend on the loader type.
        // Also, Loader#CLASS follows java.library.path, Loader#LINKER follows native linker, to search for libraries.

        Symbol getpid = foreign.downcallHandle(foreign.getSymbolAddress(IS_WINDOWS ? "GetCurrentProcessId" : "getpid"),
                IS_WINDOWS ? int.class : long.class);
        foreign.register(IS_WINDOWS ? Windows.class : POSIX.class);
        final long pid = IS_WINDOWS ? Windows.pidInt() : POSIX.pidLong();
        foreign.unregister(SymbolManagement.class); // We're able to cache the pid value, so it's okay to unregister
        Logger.defaultInstance().info("UXGL FFI", "PID: " + pid);
        Logger.defaultInstance().info("UXGL FFI", "Direct Mapping result == Handle Mapping result: " + (getpid.invokeNativeLong() == pid));
    }

    private static final class Windows {
        @Redirect("GetCurrentProcessId")     // Redirect to the real symbol name
        public static native int pidInt();
    }

    private static final class POSIX {
        @Redirect("getpid")
        public static native long pidLong(); // Like above
    }

}

Global Variables

You can also retrieve the global variable as well as get the function symbol!

package org.example.desktop.foreign;

import unrefined.nio.Allocator;
import unrefined.nio.Pointer;
import unrefined.runtime.DesktopRuntime;
import unrefined.util.UnexpectedError;
import unrefined.util.foreign.Foreign;
import unrefined.util.foreign.Library;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

public class GlobalVariables {

    public interface CLibrary extends Library {
        void fprintf(long fp, long format, int... args); // Varargs supported!
    }

    public static void main(String[] args) {
        DesktopRuntime.setup(args);              // Initialize the UXGL runtime environment
        Foreign foreign = Foreign.getInstance(); // Get the platform-dependent FFI factory

        long stdout = Allocator.defaultInstance().getAddress(foreign.getSymbolAddress("stdout")); // Get stdout FILE*

        Random random = ThreadLocalRandom.current();

        int a = random.nextInt();
        int b = random.nextInt();

        CLibrary c = foreign.downcallProxy(CLibrary.class);
        try (Pointer format = Pointer.allocateDirect("SUM (%d, %d) = %d")) {
            c.fprintf(stdout, format.address(), a, b, a + b);
        }
        catch (IOException e) {
            throw new UnexpectedError(e);
        }
    }

}
Clone this wiki locally