Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example using JNI #3321

Merged
merged 5 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 38 additions & 29 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -1205,39 +1205,48 @@ object example extends MillScalaModule {
case "javamodule" => scalamodule
}

def buildScLines = T {
val upstreamLines = os.read.lines(
upstreamCross(this.millModuleSegments.parts.dropRight(1).last)(crossValue)
.testRepoRoot().path / "build.sc"
)
val lines = os.read.lines(testRepoRoot().path / "build.sc")

import collection.mutable
val groupedLines = mutable.Map.empty[String, mutable.Buffer[String]]
var current = Option.empty[String]
lines.foreach {
case s"//// SNIPPET:$name" =>
current = Some(name)
groupedLines(name) = mutable.Buffer()
case s => groupedLines(current.get).append(s)
}
def buildScLines =
upstreamCross(
this.millModuleSegments.parts.dropRight(1).last).valuesToModules.get(List(crossValue)
) match {
case None =>
T {
super.buildScLines()
}
case Some(upstream) => T {
val upstreamLines = os.read.lines(
upstream
.testRepoRoot().path / "build.sc"
)
val lines = os.read.lines(testRepoRoot().path / "build.sc")

import collection.mutable
val groupedLines = mutable.Map.empty[String, mutable.Buffer[String]]
var current = Option.empty[String]
lines.foreach {
case s"//// SNIPPET:$name" =>
current = Some(name)
groupedLines(name) = mutable.Buffer()
case s => groupedLines(current.get).append(s)
}

upstreamLines.flatMap {
case s"//// SNIPPET:$name" =>
if (name != "END") {
upstreamLines.flatMap {
case s"//// SNIPPET:$name" =>
if (name != "END") {

current = Some(name)
groupedLines(name)
} else {
current = None
Nil
}
current = Some(name)
groupedLines(name)
} else {
current = None
Nil
}

case s =>
if (current.nonEmpty) None
else Some(s)
case s =>
if (current.nonEmpty) None
else Some(s)
}
}
}
}
}
trait ExampleCrossModule extends IntegrationTestCrossModule {
// disable scalafix because these example modules don't have sources causing it to misbehave
Expand Down
7 changes: 6 additions & 1 deletion docs/modules/ROOT/pages/Java_Module_Config.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ include::example/javamodule/10-assembly-config.adoc[]

include::example/javamodule/11-repository-config.adoc[]

== Maven Central: Blocked!
=== Maven Central: Blocked!

Under some circumstances (e.g. corporate firewalls), you may not have access maven central. The typical symptom will be error messages which look like this;

Expand All @@ -79,3 +79,8 @@ The idea is to set an environment variable COURSIER_REPOSITORIES (see coursier d

If you are using millw, a more permanent solution could be to set the environment variable at the top of the millw script, or as a user environment variable etc.



== Native C Code with JNI

include::example/javamodule/12-jni.adoc[]
59 changes: 59 additions & 0 deletions example/javamodule/12-jni/build.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import mill._, javalib._, util.Jvm

object foo extends RootModule with JavaModule {
// Additional source folder to put C sources
def nativeSources = T.sources(millSourcePath / "native-src")

// Auto-generate JNI `.h` files from Java classes using Javac
def nativeHeaders = T {
os.proc(Jvm.jdkTool("javac"), "-h", T.dest, "-d", T.dest.toString, allSourceFiles().map(_.path)).call()
PathRef(T.dest)
}

// Compile C
def nativeCompiled = T{
val cSourceFiles = nativeSources().map(_.path).flatMap(os.walk(_)).filter(_.ext == "c")
val output = "libhelloworld.so"
os.proc(
"clang", "-shared", "-fPIC",
"-I" + nativeHeaders().path, //
"-I" + sys.props("java.home") + "/include/", // global JVM header files
"-I" + sys.props("java.home") + "/include/darwin",
"-I" + sys.props("java.home") + "/include/linux",
"-o", T.dest / output,
cSourceFiles
)
.call(stdout = os.Inherit)

PathRef(T.dest / output)
}

def forkEnv = Map("HELLO_WORLD_BINARY" -> nativeCompiled().path.toString)

object test extends JavaTests with TestModule.Junit4{
def forkEnv = Map("HELLO_WORLD_BINARY" -> nativeCompiled().path.toString)
}
}

// This is an example of how use Mill to compile C code together with your Java
// code using JNI. There are three main steps: defining the C source folder,
// generating the header files using `javac`, and then compiling the C code
// using `clang`. After that we have the `libhelloworld.so` on disk ready to use,
// and in this example we use an environment variable to pass the path of that
// file to the application code to load it using `System.load`.
//
// This example is pretty minimal, but it demonstrates the core principles, and
// can be extended if necessary to more elaborate use cases. The `native*` tasks
// can also be extracted out into a `trait` for re-use if you have multiple
// `JavaModule`s that need native C components

/** Usage

> ./mill run
Hello, World!

> ./mill test
Test foo.HelloWorldTest.testSimple started
Test foo.HelloWorldTest.testSimple finished...
...
*/
8 changes: 8 additions & 0 deletions example/javamodule/12-jni/native-src/HelloWorld.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#include <jni.h>
#include "foo_HelloWorld.h"
#include <string.h>

// Implementation of the native method
JNIEXPORT jstring JNICALL Java_foo_HelloWorld_sayHello(JNIEnv *env, jobject obj) {
return (*env)->NewStringUTF(env, "Hello, World!");
}
16 changes: 16 additions & 0 deletions example/javamodule/12-jni/src/foo/HelloWorld.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package foo;

public class HelloWorld {
// Declare a native method
public native String sayHello();

// Load the native library
static {
System.load(System.getenv("HELLO_WORLD_BINARY"));
}

public static void main(String[] args) {
HelloWorld helloWorld = new HelloWorld();
System.out.println(helloWorld.sayHello());
}
}
10 changes: 10 additions & 0 deletions example/javamodule/12-jni/test/src/foo/HelloWorldTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package foo;
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class HelloWorldTest {
@Test
public void testSimple() {
assertEquals(new HelloWorld().sayHello(), "Hello, World!");
}
}
Loading