You can open this sample inside an IDE using the IntelliJ native importer or Eclipse Buildship.

This sample shows how a Java Native Interface (JNI) library can be composed of multiple projects with Gradle.

In this sample, we are composing a JNI library with components implemented in Java and C++; however, this applies to other JVM and native languages as well.

Project Structure

You can visualize the project structure and layout using the projects tasks.

$ ./gradlew projects

> Task :projects

------------------------------------------------------------
Root project - The JNI library as the consumer would expect.
------------------------------------------------------------

Root project 'jni-library-composing-from-source' - The JNI library as the consumer would expect.
+--- Project ':cpp-greeter' - The C++ implementation, has no knowledge of the JVM.
+--- Project ':cpp-jni-greeter' - The JNI shared library, also known as the native bindings.
+--- Project ':java-jni-greeter' - The JNI classes, also known as the JVM bindings.
\--- Project ':java-loader' - The library for loading the shared library from the classpath or JAR.

To see a list of the tasks of a project, run gradlew <project-path>:tasks
For example, try running gradlew :cpp-greeter:tasks

BUILD SUCCESSFUL
1 actionable task: 1 executed

The JNI library, as the root project, is composed of four projects. The following subsection will go through each one of them.

C++ Library

This library is a pure C++ library. It has no dependency or knowledge of the JVM.

build.gradle
plugins {
	id 'cpp-library'
}

description = 'The C++ implementation, has no knowledge of the JVM.'

library {
	// Note: it is possible to use a shared library.
	//     However you will need to write a loader aware of the multiple shared libraries.
	linkage = [Linkage.STATIC]
	binaries.configureEach {
		compileTask.get().positionIndependentCode = true
	}
}
build.gradle.kts
plugins {
	id("cpp-library")
}

description = "The C++ implementation, has no knowledge of the JVM."

library {
	// Note: it is possible to use a shared library.
	//     However you will need to write a loader aware of the multiple shared libraries.
	linkage.set(listOf(Linkage.STATIC))

	binaries.configureEach {
		compileTask.get().setPositionIndependentCode(true)
	}
}

It could be built by another build system altogether. In this case, we are using the Gradle C++ library core plugin.

Java/C++ JNI Bindings

The JNI bindings are split into two separate projects. Each project is fulfilling two sides of the same coin, the JNI bindings on the JVM and the native side, respectively.

The JVM JNI binding project defines the classes with methods marked with the native keyword. We are using the Gradle Java library core plugin.

build.gradle
plugins {
	id 'java-library'
}

description = 'The JNI classes, also known as the JVM bindings.'

dependencies {
	implementation project(':java-loader')
}
build.gradle.kts
plugins {
	id("java-library")
}

description = "The JNI classes, also known as the JVM bindings."

dependencies {
	implementation(project(":java-loader"))
}

The native JNI binding project is coupled to the JVM. It uses JVM types provided by the jni.h header. In this sample, we are assuming the user would have generated the JNI header manually to be used within the project. It is considered bad practice to generate the JNI headers manually. The JNI Library Plugin will automatically generate the JNI headers and wire them properly to the native compilation task when both JNI binding project is inside the same project. See the Java/C++ JNI library sample for a demonstration of this feature. We are using the Gradle C++ library core plugin.

build.gradle
plugins {
	id 'cpp-library'
}

description = 'The JNI shared library, also known as the native bindings.'

import org.gradle.internal.jvm.Jvm

library {
	// The native component of the JNI library needs to be a shared library.
	linkage = [Linkage.SHARED]
	dependencies {
		implementation project(':cpp-greeter')
	}

	binaries.configureEach {
		compileTask.get().getIncludes().from(compileTask.get().targetPlatform.map {
			def result = [new File("${Jvm.current().javaHome.absolutePath}/include")]

			if (it.operatingSystem.macOsX) {
				result.add(new File("${Jvm.current().javaHome.absolutePath}/include/darwin"))
			} else if (it.operatingSystem.linux) {
				result.add(new File("${Jvm.current().javaHome.absolutePath}/include/linux"))
			} else if (it.operatingSystem.windows) {
				result.add(new File("${Jvm.current().javaHome.absolutePath}/include/win32"))
			}
			return result
		})
		compileTask.get().positionIndependentCode = true
	}
}
build.gradle.kts
import org.gradle.internal.jvm.Jvm

plugins {
	id("cpp-library")
}

description = "The JNI shared library, also known as the native bindings."

library {
	// The native component of the JNI library needs to be a shared library.
	linkage.set(listOf(Linkage.SHARED))
	dependencies {
		implementation(project(":cpp-greeter"))
	}

	binaries.configureEach {
		compileTask.get().includes.from(compileTask.get().targetPlatform.map {
			listOf(File("${Jvm.current().javaHome.canonicalPath}/include")) + when {
				it.operatingSystem.isMacOsX -> listOf(File("${Jvm.current().javaHome.absolutePath}/include/darwin"))
				it.operatingSystem.isLinux -> listOf(File("${Jvm.current().javaHome.absolutePath}/include/linux"))
				it.operatingSystem.isWindows -> listOf(File("${Jvm.current().javaHome.absolutePath}/include/win32"))
				else -> emptyList()
			}
		})
		compileTask.get().setPositionIndependentCode(true)
	}
}
The samples use Jvm class, which is an internal type to Gradle. This type was used as conveniences and demonstration purposes. It should not be used in production.

Java Library

This library is a pure Java library supporting the JNI binding on the JVM side. It could be any API or implementation dependencies.

build.gradle
plugins {
	id 'java-library'
}

description = 'The library for loading the shared library from the classpath or JAR.'
build.gradle.kts
plugins {
	id("java-library")
}

description = "The library for loading the shared library from the classpath or JAR."

Assembling the JNI Library

To build the library:

$ ./gradlew assemble

BUILD SUCCESSFUL
9 actionable tasks: 9 executed

All the native runtime dependencies will be packaged inside the generated JAR:

$ jar tf ./build/libs/jni-library-composing-from-source.jar
META-INF/
META-INF/MANIFEST.MF
libcpp-jni-greeter.dylib

The JNI JAR is also registered as an outgoing variant:

$ ./gradlew outgoingVariants

> Task :outgoingVariants
--------------------------------------------------
Variant apiElements
--------------------------------------------------
Description = API elements for main.

Capabilities
    - :jni-library-composing-from-source:unspecified (default capability)
Attributes
    - org.gradle.libraryelements = jar
    - org.gradle.usage           = java-api

--------------------------------------------------
Variant runtimeElements
--------------------------------------------------
Description = Elements of runtime for main.

Capabilities
    - :jni-library-composing-from-source:unspecified (default capability)
Attributes
    - org.gradle.libraryelements = jar
    - org.gradle.usage           = java-runtime

Artifacts
    - build/libs/jni-library-composing-from-source.jar (artifactType = jar)


BUILD SUCCESSFUL
1 actionable task: 1 executed

For more information, see JNI Library Plugin and C++ Language Plugin reference chapters.