Introduction
This repository contains a set of simple projects to experiment with Java interacting with native code.
A blog post describing this project can be found on my https://lofthouse.dev site.
Projects
This repository uses the following projects:
- simple-library - A simple C library installing both a static and a dynamic variant.
- simple-c-app - A simple C app linked to the
simple-library, the build can be configured to use either the static or the dynamic variant. - simple-jni - A simple Java project with two defined native methods.
- jni-library - Minimal dynamic library, implementing the generated header from
simple-jnito call the library fromsimple-library. - simple-foreign - A simple Java project calling
simple-librarydirectly using the new foreign functions APIs.
Building
simple-library
All other projects depend on simple-library, so build it first.
mkdir simple-library/build && cd simple-library/build cmake .. make make install
CMake detects $HOME/local as the install prefix; make install creates the lib/ and include/ subdirectories automatically.
Verifying the install
Check the installed files exist:
ls $HOME/local/lib/libsimple-library.so ls $HOME/local/lib/libsimple-library-static.a ls $HOME/local/include/simple-library.h
Verify the exported symbols in the shared library:
nm -D $HOME/local/lib/libsimple-library.so | grep -E 'add_one|say_hello'
Expected output: two T (text/code) symbols — add_one and say_hello:
0000000000001109 T add_one
0000000000001118 T say_hello
Check the shared library's runtime dependencies:
ldd $HOME/local/lib/libsimple-library.soExpected: only libc.so (no extra dependencies).
simple-c-app
Prerequisite: simple-library must be built and installed first.
mkdir simple-c-app/build && cd simple-c-app/build cmake .. make
Note: there is no make install — the executable lives in build/.
Running
From the simple-c-app/ directory:
cd simple-c-app
./run-app.shExpected output:
Hello, from simple-c-app!
add_one(5) = 6
Hello, from simple-library!
simple-jni (Maven — generates JNI headers)
Prerequisite: simple-library must be built and installed first.
Important: Run this Maven build from inside the simple-jni/ directory, not from the
repository root with -f. The compiler plugin generates JNI C headers to target/include/
relative to the working directory, and jni-library's CMake build looks for them at
../simple-jni/target/include/.
cd simple-jni
mvn clean packageVerify the headers were generated:
ls simple-jni/target/include/
# dev_lofthouse_App.hjni-library
Prerequisites:
simple-librarybuilt and installed ($HOME/local/lib/libsimple-library.so)simple-jniMaven build completed (generatessimple-jni/target/include/dev_lofthouse_App.h)
mkdir jni-library/build && cd jni-library/build cmake .. make make install
make install copies libjni-library.so to $HOME/local/lib/.
simple-jni (JVM run)
Prerequisite: jni-library must be built and installed to $HOME/local/lib/.
From the simple-jni/ directory:
cd simple-jni
./run-app.shExpected output:
Java says Hello World!
<java.library.path — varies by machine>
addOne(11)= 12
Java says Goodbye World!
Hello, from simple-library!
Note:
Hello, from simple-library!appears afterJava says Goodbye World!due to stdout buffering differences between Java'sPrintStreamand C'sprintf.
simple-foreign
Prerequisite: simple-library must be built and installed first.
cd simple-foreign
mvn clean package
./run-app.shExpected output:
addOne(14) = 15
Hello World!
Hello, from simple-library!
Note:
say_hello()is invoked beforeadd_one()in the source, butHello, from simple-library!appears last due to stdout buffering differences between Java'sPrintStreamand C'sprintf.
simple-jni (native image)
Prerequisite: jni-library must be built and installed to $HOME/local/lib/.
GraalVM is required (not included in standard Temurin/OpenJDK distributions).
From the simple-jni/ directory:
cd simple-jni
mvn clean package -Dnative
./run-app-native.shExpected output:
Java says Hello World!
<LD_LIBRARY_PATH — varies by machine>
addOne(11)= 12
Java says Goodbye World!
Hello, from simple-library!
Note:
Hello, from simple-library!appears afterJava says Goodbye World!due to stdout buffering differences between Java'sPrintStreamand C'sprintf.
simple-foreign (native image)
Prerequisite: simple-library must be built and installed first.
GraalVM is required (not included in standard Temurin/OpenJDK distributions).
From the simple-foreign/ directory:
cd simple-foreign
mvn clean package -Dnative
./run-app-native.shsimple-foreign includes a ForeignRegistrationFeature (in
src/main/java/dev/lofthouse/graal/) that registers the two Foreign Function API downcall
stubs (add_one and say_hello) at build time. GraalVM's Substrate VM cannot discover these
at runtime, so they must be declared explicitly for AOT compilation. The feature is activated
via META-INF/native-image/native-image.properties.
Expected output:
addOne(14) = 15
Hello World!
Hello, from simple-library!
Note:
say_hello()is invoked beforeadd_one()in the source, butHello, from simple-library!appears last due to stdout buffering differences between Java'sPrintStreamand C'sprintf.
Java projects — common notes
# Build and run tests mvn clean package # Build with GraalVM native image mvn clean package -Dnative # Run tests only mvn test
Each Java project provides run-app.sh (JVM) and run-app-native.sh (native image) scripts.
Prerequisites: Java 25 is required. GraalVM is required for native image builds (the -Dnative profile).