Skip to content

C header bindings

Generate Java Foreign Function and Memory API bindings from C headers with elide jextract, then compile and run them against a native shared library.

On this page

elide jextract reads C headers and generates Java source files using Java’s Foreign Function and Memory (FFM) API. It embeds OpenJDK jextract and shares Elide’s Clang/LLVM native library with the embedded C compiler and linker, so generation needs no separate jextract, JDK or libclang installation.

Distribution support

Native tools are part of Elide 1.6.2. They are included on macOS arm64 and dynamically linked Linux amd64/arm64, and omitted on Windows, fully static Elide builds and custom builds using --no-jvm-toolchain.

Use this command when you want generated .java files to inspect, distribute or compile separately. For an Elide Java project, C header bindings in the Java compiler can generate and compile bindings in memory from Pkl declarations or ad hoc elide javac options.

Generate Java source

Save a small library interface as native/answer.h:

int answer(void);

Generate its Java bindings:

elide jextract -- --output generated \
  --target-package example.ffi \
  --header-class-name NativeApi \
  native/answer.h

The output includes generated/example/ffi/NativeApi.java and any supporting sources needed by the bindings. The header’s answer declaration becomes a Java accessor such as NativeApi.answer(). Generation produces source code, not a native library, class files or JNI wrappers. It does not load the application library, so its implementation does not need to be present at this step.

Without --output, sources go in the current directory. Without --target-package, they use the unnamed package. Without --header-class-name, jextract derives a class name from the header filename, such as answer_h for answer.h. Supply an explicit header class name when passing multiple headers.

Configure parsing and select declarations

Pass jextract’s arguments after --; its own help lists the full interface:

elide jextract -- --help
OptionPurpose
--output PATHDirectory for generated Java sources.
-t PACKAGE, --target-package PACKAGEPackage for generated classes.
--header-class-name NAMEGenerated header class name.
-I DIR, --include-dir DIRAdd a C header search directory; repeat for multiple directories.
-D NAME=VALUE, --define-macro NAME=VALUEDefine a preprocessor macro; repeat as needed.
--include-function NAMESelect a function for generation.
--include-constant NAMESelect a macro or enum constant.
--include-struct NAME, --include-union NAMESelect a struct or union.
--include-typedef NAME, --include-var NAMESelect a typedef or global variable.
-l LIBSPEC, --library LIBSPECAdd a shared library to the generated runtime lookup.
--use-system-load-libraryUse System.loadLibrary or System.load for those libraries.

For example, add a library’s include directory, set a macro and select one function:

elide jextract -- --output generated -t example.ffi \
  --header-class-name NativeApi \
  -I native/include -D ANSWER_API_VERSION=1 \
  --include-function answer native/answer.h

The standalone command also reads Clang arguments from compile_flags.txt in the current working directory, one argument per line. This differs from the integrated Java compilation path, which uses its declared binding configuration. Elide passes jextract’s diagnostics and exit status through; a failed extraction does not yield usable bindings.

Call a shared library

Suppose native/libanswer.so on Linux, or native/libanswer.dylib on macOS, exports the implementation:

int answer(void) {
  return 42;
}

The library must be built for the machine running the Java application. Obtain it from the library’s build or package, or compile native/answer.c with a separate full C compiler:

# Linux
clang -shared -fPIC native/answer.c -o native/libanswer.so

# macOS
clang -dynamiclib native/answer.c -o native/libanswer.dylib

These are external compiler commands. elide cc exposes object/IR output and in-memory JIT linking; its current CLI does not expose shared-library output.

Regenerate the bindings with an explicit library path. On Linux:

elide jextract -- --output generated -t example.ffi \
  --header-class-name NativeApi \
  -l :./native/libanswer.so native/answer.h

On macOS, use -l :./native/libanswer.dylib instead. The leading : marks a path rather than a logical library name. A relative library path is resolved when the generated code runs, so run this example from the directory containing native/.

Save this caller as Main.java:

import example.ffi.NativeApi;

public class Main {
  public static void main(String[] args) {
    System.out.println(NativeApi.answer());
  }
}

Compile the generated sources and caller with Elide’s bundled Java 25 compiler, then enable native access when running them:

elide javac -- -d out generated/example/ffi/*.java Main.java
elide java -- --enable-native-access=ALL-UNNAMED -cp out Main
42

ALL-UNNAMED applies to this classpath example. For a named module, enable native access for the module containing the generated bindings. Native access is needed by the generated FFM code at runtime; successful generation and Java compilation do not establish that the shared library can be loaded.

Library discovery and memory ownership

By default, -l answer generates a SymbolLookup.libraryLookup for the platform-mapped library name, such as libanswer.so or libanswer.dylib. Its discovery follows the platform loader’s rules. -l :PATH embeds the path instead. Repeat -l for multiple libraries. The generated lookup also falls back to the class loader’s symbols and the native linker’s default symbols.

For Java’s java.library.path lookup, generate with --use-system-load-library -l answer, then supply -Djava.library.path=./native to the Java launcher. This emits System.loadLibrary("answer"); a path library specification instead uses System.load and requires an absolute path. Header include directories set with -I do not configure runtime library discovery.

Elide does not package the application library with generated sources or JARs. Keep the matching shared library and its dependencies available at runtime. Generating FFM bindings also does not automatically register those calls for an arbitrary Native Image application.

Generated bindings expose native pointers through MemorySegment and provide layouts for supported C types. They do not infer ownership rules: use an appropriate Arena for Java-managed native allocations, keep allocations and callback stubs alive while C uses them, and release library-owned memory through the library’s documented API. Native code runs with the process’s privileges, and incorrect lifetimes or ABI mismatches can crash the process.

Headers and platform limits

Elide supplies jextract’s builtin Clang headers. Library-specific headers and their dependencies still need to be available through the supplied paths and include directories. This is a C header generator; C++ interfaces need a C-compatible API. Review warnings about unsupported declarations rather than assuming every header construct becomes a Java type. Opaque structs can remain usable as pointer handles even when their layouts cannot be generated.

The macOS distribution does not include an Apple SDK. SDK-dependent headers require a compatible installed SDK and appropriate include configuration. Elide’s packaged Native Image compiler/sysroot configuration is a separate facility; it does not make every host header available to jextract. The self-contained header above requires no system SDK.

Was this page helpful?