Bind Java code and Managed C Code

The Core Engine features a direct connection between Java code and Managed C code through a bidirectional binding between Java methods and Managed C functions. Also, the Java code can directly access the Managed C linear memory via the getMemory() method inherited from WasmModule.

Principle

The following figure illustrates the binding between Java methods and C functions through a first code example.

Basically, a Java instance method declared as native is bound to a Managed C function with the same name. Conversely, a Managed C function declared as extern is bound to either a Java instance method of the same WasmModule subclass, or to a static Java method of any class.

Here is a first example:

../../_images/managedc-communication-example.png

The next figure illustrates the call stack of the Core Engine main thread:

../../_images/managedc-communication-callstack.png

You can notice that, from Core Engine execution point of view, there is no distinction between Java methods and Managed C functions. The execution of the main entry point produces the following stack trace:

Exception in thread "main" java.lang.RuntimeException: Core Engine Stack Trace
   [...]
   at java.lang.RuntimeException.<init>(RuntimeException.java:18)
   at example.JavaCode.function2(JavaCode.java:13)
   at example.JavaCode.function1(Unknown Source)
   at example.JavaCode.main(JavaCode.java:26)
   at java.lang.MainThread.run(Thread.java:856)
   at java.lang.Thread.runWrapper(Thread.java:465)
   [...]

The line example.JavaCode.function1 represents the execution of the Managed C function named function1.

Note

The Core Engine prints Unknown Source because the Wasm file in this example is built without debug information. Embedding debug information using the -g compiler option enables the retrieval of line numbers in C files.

Declare a Wasm Module

To use Managed C, create a Java class that:

  1. Extends WasmModule.

  2. Is declared final.

  3. Optionally carries the @Alias annotation with the module name. The SOAR resolves the corresponding .wasm file from the MicroEJ Classpath by appending the .wasm extension to the alias name. If no @Alias annotation is present, the simple class name is used.

Drop your Wasm module file into src/main/resources. Let’s assume it is called my_app.wasm.

Add to the desired Java class the @Alias annotation with the module name (e.g. @Alias("my_app")).

Your class should look like the following code:

package com.mycompany;

import ej.lang.Alias;
import ej.lang.WasmModule;

// This Java class is bound to a Wasm module named 'my_app' and
// loaded from the '/my_app.wasm' resource in MicroEJ classpath.
@Alias("my_app")
public final class MyApp extends WasmModule {

}

Your project files should look like the following:

├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── mycompany
│       │           └── MyApp.java
│       └── resources
│           └── my_app.wasm

To place the Wasm file in a sub-directory, include the relative path directly in the @Alias value. For example, if my_app.wasm is located under src/main/resources/wasm/:

package com.mycompany;

import ej.lang.Alias;
import ej.lang.WasmModule;

// Loaded from the '/wasm/my_app.wasm' resource in MicroEJ classpath.
@Alias("wasm/my_app")
public final class MyApp extends WasmModule {

}
├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── mycompany
│       │           └── MyApp.java
│       └── resources
│           └── wasm
│               └── my_app.wasm

Bind a Java Method to a Managed C Function

Once a Java class declares a Wasm module, all Java instance methods and Managed C functions that meet the following conditions are automatically bound:

  • The Java method is an instance method (not static).

  • The signature (name, parameters and return type) of the Java method matches with the signature of the Managed C function (see Matching Types).

  • The Managed C function has been exported by the Wasm module. See -Wl,--export linker option. (Managed C functions declared static cannot be exported as they are only visible in the C file they are declared) [1].

Here is an example:

  • Java source code (MyApp.java):

    package com.mycompany;
    
    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app")
    public final class MyApp extends WasmModule {
    
       // Bound to Managed C function 'print(int)' in 'my_app.wasm' module
       public void print(int c) {
          System.out.print((char) c);
       }
    
    }
    
  • C source code (my_app.c):

    // Bound to Java method 'com.mycompany.MyApp.print(int)'
    extern void print(int c);
    

The following sections explain how to customize the default binding from the Java code side and from the C code side.

Bind a Managed C Function from Java Code

The @Alias annotation is used to extend the default Java Method to Managed C Function binding from the Java code side.

Call a Managed C Function from Java Code

The most common case is to call a Managed C function from Java code.

For that, the Java method being bound must be:

  • An instance method (not static).

  • Declared native, as it does not include a body.

  • Declared synchronized to prevent concurrent execution of Managed C code on the same module instance by default, as stated by the WebAssembly 1.0 specification.

Note

POSIX pthreads are supported through WASI Threads. Threads execute multiple Wasm Module instances that share a common linear memory.

Here is an example:

  • Java source code (MyApp.java):

    package com.mycompany;
    
    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app")
    public final class MyApp extends WasmModule {
    
       public static void main(String[] args) {
          MyApp myApp = new MyApp();
          int a = 1;
          int b = 2;
          // Call and return result of the "add" Managed C function
          int r = myApp.add(a, b);
    
          System.out.println(a + " + " + b + " = " + r);
       }
    
       public native synchronized int add(int a, int b);
    
    }
    
  • C source code (my_app.c):

    int add(int a, int b) {
       return a + b;
    }
    

You should see the following output when launching the Application:

1 + 2 = 3

Bind a Managed C Function with a Different Name

The @Alias annotation can also bind a Java method to a Managed C function that has a different name. Provide the C function name as the parameter of the @Alias annotation. This is especially useful if you want to write a Java method name in camelCase while binding it to a Managed C function name written in snake_case.

  • Java source code (MyApp.java):

    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app")
    public final class Main extends WasmModule {
    
       public static void main(String[] args) {
          Main myApp = new Main();
          int a = 1;
          int b = 2;
          // Call and return result of the "do_add" Managed C function
          System.out.println(a + " + " + b + " = " + myApp.doAdd(a, b));
       }
    
       @Alias("do_add")
       public native synchronized int doAdd(int a, int b);
    }
    
  • C source code (my_app.c):

    int do_add(int a, int b) {
       return a + b;
    }
    

Bind a Java Method from C Code

The import_module, import_name and export_name C attributes are used to extend the default Java Method to Managed C Function binding from C code side.

Call a Java Method from C Code

From C code, an extern function can be bound to either:

  • A Java instance method of the WasmModule subclass bound to the same Wasm module.

  • A static Java method of any class.

The import_module attribute is used when the Java method to be bound belongs to a Java class other than the one bound to the Wasm module. The module name can be either the fully qualified name of the Java class containing the Java method (e.g: com.mycompany.MyApp) or the alias name of the @Alias annotation of the class containing the Java method.

The most common case is to call a static Java method declared in a library from C code. The corresponding Java class fully qualified name is provided as a parameter of the __import_module__ attribute.

Here is an example with the standard Java Math.max(int,int) method.

  • C source code (my_app.c):

    // Binding to Java static method 'java.lang.Math.max(int,int)'
    __attribute__((__import_module__("java.lang.Math")))
    extern int max(int i, int j);
    

Use @Alias to Simplify C-to-Java Bindings

The @Alias annotation can be applied to any Java class or method, not only to WasmModule subclasses. This allows C code to reference a Java class and method using short, C-friendly names instead of the fully qualified class name and the camelCase Java method name.

Here is an example with a MathUtils utility class:

  • Java source code (MathUtils.java):

    package com.mycompany;
    
    import ej.lang.Alias;
    
    @Alias("utils")
    public class MathUtils {
    
       /**
        * Clamps the given value to the range {@code [min, max]}.
        *
        * @param value
        *            the value to clamp.
        * @param min
        *            the lower bound.
        * @param max
        *            the upper bound.
        * @return the clamped value.
        */
       @Alias("clamp")
       public static int clampValue(int value, int min, int max) {
          if (value < min) {
             return min;
          }
          if (value > max) {
             return max;
          }
          return value;
       }
    
    }
    
  • C source code (my_app.c):

    // Without @Alias, __import_module__ would require the fully qualified
    // name "com.mycompany.MathUtils" and __import_name__ would require "clampValue".
    __attribute__((__import_module__("utils"), __import_name__("clamp")))
    extern int clamp(int value, int min, int max);
    
    void app_main() {
       int result = clamp(150, 0, 100); // result == 100
    }
    

Pass the Calling Module as a Parameter

When a Managed C function calls a static Java method, the SOAR can automatically pass the calling WasmModule instance as an implicit first argument. To enable this, declare the first parameter of the static Java method as a WasmModule. The C function signature does not include this parameter — it remains invisible from C.

This allows utility classes that do not extend WasmModule to receive the calling module and interact with it — for example, to call one of its methods, or to access its linear memory.

Here is an example where a standalone MessageHandler class calls a method on the module:

  • Java source code (MessageHandler.java and MyApp.java):

    package com.mycompany;
    
    import ej.lang.WasmModule;
    
    public class MessageHandler {
    
       private static final String[] MESSAGES = { "Success", "Warning", "Error" };
    
       /**
        * Called from Managed C code when a message is available.
        * <p>
        * The {@code module} parameter is automatically injected by the SOAR
        * and does not appear in the C function signature.
        *
        * @param wasmModule
        *            the calling Wasm module, injected by the SOAR.
        * @param code
        *            the message code.
        */
       public static void onMessage(WasmModule wasmModule, int code) {
          ((MyApp) wasmModule).displayMessage(MESSAGES[code]);
       }
    
    }
    
    package com.mycompany;
    
    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app")
    public final class MyApp extends WasmModule {
    
       public static void main(String[] args) {
          MyApp myApp = new MyApp();
          myApp.appMain();
       }
    
       public native synchronized void appMain();
    
       /**
        * Displays the given message.
        *
        * @param message
        *            the message to display.
        */
       public void displayMessage(String message) {
          System.out.println(message);
       }
    
    }
    
  • C source code (my_app.c):

    // The Java signature has WasmModule as first parameter, but it does not
    // appear in the C function signature — the SOAR injects it automatically.
    __attribute__((__import_module__("com.mycompany.MessageHandler")))
    extern void onMessage(int code);
    
    void appMain() {
       onMessage(0); // Sends "Success"
       onMessage(2); // Sends "Error"
    }
    

You should see the following output when launching the Application:

Success
Error

Bind a Java Method with a Different Name

The import_name attribute is used when the Managed C function name has a different name from the Java method. The corresponding Java method name is provided as a parameter of the __import_name__ attribute. This is especially useful if you want to write a Managed C function in snake case while binding it to a Java method name written in camelCase.

  • Java source code (MyApp.java):

    package com.mycompany;
    
    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app")
    public final class MyApp extends WasmModule {
    
       public void javaPrint(int c) {
          System.out.print((char) c);
       }
    
    }
    
  • C source code (my_app.c):

    __attribute__((__import_name__("javaPrint")))
    extern void c_print(int c);
    

A Managed C extern function can also be bound to a static native Java method that is an SNI method implemented in unmanaged C code. This makes it possible for Managed C code to call unmanaged C code: the Managed C function calls the Java SNI method via the extern binding, and the SNI method itself is implemented by a native C function outside the Wasm module.

  • Java source code (MyApp.java):

    package com.mycompany;
    
    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app_managed")
    public final class MyApp extends WasmModule {
    
       // static native → SNI method implemented in unmanaged C
       public static native void javaPrint(int c);
    
    }
    
  • Managed C source code (my_app_managed.c):

    __attribute__((__import_name__("javaPrint")))
    extern void c_print(int c);
    
  • Unmanaged C source code (my_app_native.c):

    #include "sni.h"
    #include "stdio.h"
    
    void Java_com_mycompany_javaPrint(jint c) {
       putchar(c);
    }
    

The export_name attribute is used for the same reason, when the Managed C function implements the code.

  • Java source code (MyApp.java):

    package com.mycompany;
    
    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app")
    public final class MyApp extends WasmModule {
    
       public native synchronized void javaPrint(int c);
    
    }
    
  • C source code (my_app.c):

    #include <stdio.h>
    
    __attribute__((__export_name__("javaPrint")))
    void c_print(int c) {
       putchar(c);
    }
    

Matching Types

Java methods bound to Managed C functions can only use Java base types int, long, float, double as parameters and return types.

Here is the matching table:

Managed C Types / Java Types matching

C Std Type

Java Type

Wasm Type

int32_t

int

i32

int64_t

long

i64

float32_t

float

f32

float64_t

double

f64

Any pointer

int

i32

SOAR will trigger an error if Managed C function parameter(s) and return types do not match exactly the Java method parameter(s) and return types.

Access Managed C Memory from Java

The Managed C memory is composed of all static arrays, structures, globals declared in C files, thread stacks and optional heap for dynamic allocations. All these elements are linked to the Wasm module linear memory.

On the Java side, the Wasm module linear memory is accessible via the getMemory() method inherited from WasmModule. Call it on the module instance to retrieve the backing byte[] array.

Here is an example of an instance method in the WasmModule subclass reading the linear memory directly:

  • C source code (my_app.c):

    typedef unsigned char uint8_t;
    extern void printValues(int ptr, int size);
    
    uint8_t array[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    void appMain() {
       printValues((int)&array[0], sizeof(array));
    }
    
  • Java source code (MyApp.java):

    package com.mycompany;
    
    import ej.lang.Alias;
    import ej.lang.WasmModule;
    
    @Alias("my_app")
    public final class MyApp extends WasmModule {
    
       public static void main(String[] args) {
          MyApp myApp = new MyApp();
          myApp.appMain();
       }
    
       /**
        * Managed C entry point.
        */
       public native synchronized void appMain();
    
       /**
        * Prints values stored in the linear memory.
        *
        * @param ptr
        *            index in the Managed C memory.
        * @param length
        *            number of bytes to print.
        */
       public void printValues(int ptr, int length) {
          byte[] memory = this.getMemory();
          System.out.println("Memory values from " + ptr + " to " + (ptr + length) + ":");
          for (int i = 0; i < length - 1; i++) {
             System.out.print(memory[ptr + i] + ", ");
          }
          System.out.println(memory[ptr + (length - 1)]);
       }
    
    }
    

You should see the following output when launching the Application:

Memory values from 1024 to 1034:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10

Note

To access the linear memory from a utility class outside the WasmModule subclass, declare the static Java method with a WasmModule first parameter. The SOAR automatically injects the calling module instance. See Pass the Calling Module as a Parameter for a complete example.

Start Function

When a Wasm module declares a start function, it is automatically executed during the construction of the WasmModule instance (i.e., inside the WasmModule() constructor).