Skip to content
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
/test/output_rust*/Cargo.lock
/test/run_rust/target/
/test/run_rust/Cargo.lock
/test/output_java*/classes/
/test/output_java*/sources.txt
/test/run_java/classes/
parallel-hashmap/
/examples/cxx.txt
/examples/c/output
Expand All @@ -29,5 +32,8 @@ parallel-hashmap/
/examples/rust/library/Cargo.lock
/examples/rust/example_consumer/target
/examples/rust/example_consumer/Cargo.lock
/examples/java/c_library
/examples/java/library
/examples/java/example_consumer/classes
/examples/python/pybind11
/examples/python/output
21 changes: 20 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ endif()
option(MRBIND_BUILD_GENERATOR_C "Build the C binding generator." ON)
option(MRBIND_BUILD_GENERATOR_CSHARP "Build the C# binding generator." ON)
option(MRBIND_BUILD_GENERATOR_RUST "Build the Rust binding generator." ON)
option(MRBIND_BUILD_GENERATOR_JAVA "Build the Java binding generator." ON)

if(MRBIND_BUILD_GENERATOR_C OR MRBIND_BUILD_GENERATOR_CSHARP OR MRBIND_BUILD_GENERATOR_RUST)
if(MRBIND_BUILD_GENERATOR_C OR MRBIND_BUILD_GENERATOR_CSHARP OR MRBIND_BUILD_GENERATOR_RUST OR MRBIND_BUILD_GENERATOR_JAVA)
add_library(mrbind_gen_common STATIC
src/generators/common/data_from_json.cpp
src/generators/common/load_file.cpp
Expand Down Expand Up @@ -237,3 +238,21 @@ if(MRBIND_BUILD_GENERATOR_RUST)
apply_static_stdlib(mrbind_gen_rust)
endif()
endif()

if(MRBIND_BUILD_GENERATOR_JAVA)
add_executable(mrbind_gen_java
src/generators/java/generator.cpp
src/generators/java/main.cpp
)
target_include_directories(mrbind_gen_java PRIVATE
deps/cppdecl/include
)
target_link_libraries(mrbind_gen_java PRIVATE
mrbind_common
mrbind_gen_common
mrbind_c_interop
)
if(MRBIND_STATIC_BUILD)
apply_static_stdlib(mrbind_gen_java)
endif()
endif()
99 changes: 99 additions & 0 deletions docs/generating_java.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Generating Java bindings

We generate Java sources that call the C bindings through the foreign function API (`java.lang.foreign`, final since Java 22), so you're required to [generate our C bindings first](/docs/generating_c.md). There's no JNI glue to compile.

The sources are portable, but they load the C shared library at runtime, so you must compile the C bindings for every platform you want to support. Only 64-bit platforms are supported.

## Generating the C bindings

Pass `--output-desc-json c_desc.json` to the C generator to make it dump the C bindings description, which the Java generator consumes. The description includes the C signature of every generated function, so the Java generator never needs to see the C headers.

You must also pass `--force-emit-common-helpers`, since the generated Java code calls the common helper functions (the exception handler).

It's recommended to pass [`--copy-inherited-members`](/docs/generating_c.md#inherit-class-members) too, otherwise the derived classes won't have the methods of their bases.

## Running the Java generator

The Java bindings are produced by a separate executable, `build/mrbind_gen_java`:
```sh
build/mrbind_gen_java \
--input-json c_desc.json \
--output-dir mylib/src \
--package com.example.mylib \
--imported-lib-name MyLib \
--force-namespace MyLib
```
Where:

* `c_desc.json` is the file produced by the C generator in response to `--output-desc-json`.

* `--output-dir mylib/src` is the root of the generated sources, the package directories are created in it.

The default behavior is to error if this directory is not empty, pass `--clean-output-dir` to delete the contents instead.

* `--package com.example.mylib` is the base Java package. The runtime support classes go directly into it, so it should be dedicated to the bindings. The C++ namespaces become its subpackages, and the raw downcall handles go to its `ffi` subpackage.

* `--imported-lib-name MyLib` is the C shared library name that we'll load at runtime with `System.loadLibrary("MyLib")`. It must be produced by compiling the code produced by our C bindings generator.

Don't add any extensions, and don't add the `lib` prefix.

If you used `--split-library` in the C generator, pass `--imported-split-lib-name <macro_prefix> <name>` for every part, like with the C# generator.

* `--force-namespace MyLib` is optional, but you'll usually want it. It puts the entities of the global C++ namespace (which includes the `std::` container instantiations) into the package of this namespace (`com.example.mylib.mylib` here, `Foo::Bar` becomes `foo.bar`). Without it they go directly to the base package, next to the runtime classes.

Class names longer than 100 characters (some `std::function` instantiations) are truncated and suffixed with a hash, since each class is a file and Windows limits the path length. `--max-class-name-length` changes the limit, 0 disables it.

The generator prints a summary at the end: how many functions got wrappers, how many of those use raw C types, and how many were skipped because their C types can't be spelled in Java (`long double` and such). Pass `--verbose` to list the skipped ones.

### Compiling and running

Compile the sources with any JDK 22 or newer. There are usually too many files for the command line, so pass them through a file:
```sh
find mylib/src -name '*.java' > sources.txt
javac --release 22 -d mylib/classes @sources.txt
jar --create --file mylib.jar -C mylib/classes .
```
The generated code has no dependencies. It compiles with `-Xlint:all -Werror`.

At runtime the C bindings library must be in `java.library.path` (`-Djava.library.path=/path/to/c/bindings`), or at least loadable by its file name through the system library search path. The foreign function API is a restricted API, so pass `--enable-native-access=ALL-UNNAMED` (or the name of your module) to `java` to avoid a warning on the first call.

## What the generated code looks like

A C++ namespace becomes a Java package named after it in lower case (`MR::Foo` → `mr.foo`), nested in the base package. A class in it becomes a Java class of the same name (nested C++ classes are flattened: `A::B` → `A_B`, template instantiations and `std::` types get the names of their C types minus the prefix: `std_vector_int`). The free functions of a namespace are the static methods of its `Functions` class.

The `ffi` subpackage mirrors the file hierarchy of the C bindings: for every C source `foo/bar.h` there's a `ffi/foo/bar.java` holding the downcall `MethodHandle` of every function of that file (in nested classes of 200 handles each, to keep the static initializers small). You don't normally name those, the wrappers do.

The runtime support classes live in the base package: `Ffi` (library loading, the struct layout helper, the exception handler), `CppObject`, `CppStruct`, `Native` and `CppException`.

The wrappers:

* Classes that need to live on the heap are opaque, their wrappers hold a pointer. A wrapper created by a constructor or returned by value *owns* the C++ object: it's destroyed when the wrapper is garbage-collected (through `java.lang.ref.Cleaner`), or earlier when `close()` is called (the wrappers are `AutoCloseable`, so `try (var mesh = ...)` works). A wrapper returned by reference doesn't own the object, and keeps alive what the reference points into, so it doesn't dangle: the objects named by the `[[clang::lifetimebound]]` annotations (and by the lifetimes the C generator infers, e.g. for field getters), or without annotations `this` for methods and the only object parameter for free functions.

* Parameters take wrappers: `const T &`, `T &` and by-value copies all take a `T`. Pointers and parameters with default arguments are nullable, `null` passes a null pointer or asks for the default argument. `std::optional<T>` parameters are nullable too. Passing a non-copyable class by value moves from the wrapper, which can't be used after that (see `CppObject.release()`).

* `std::string`, `std::string_view` and `std::filesystem::path` parameters take a `String`. Returned strings are `std_string` wrappers, whose `toString()` decodes them as UTF-8.

* Scalars by const pointer or reference take the Java primitive (boxed when nullable), and are returned as the primitive. Scalars by non-const pointer stay raw, Java can't write through them.

* Enums are classes with the constants as static fields, and the underlying value in the `value` field, so they can also hold values that aren't named constants. `E.of(value)` finds the constant.

* The structs exposed with `--expose-as-struct` are views over native memory with the C++ layout: the fields are accessed in place with `x()`/`setX()` (arrays with an index), `copy()` makes an independent copy, `set()` overwrites the contents. Instances created in Java own their memory, those returned by reference point into the memory of their owner. The layout is built at runtime with the C alignment rules, and checked against the sizes the C bindings were generated with when the struct has no platform-sized fields.

* A class with bases gets `upcastTo<Base>()`, `staticDowncastFrom<Base>(base)` for the non-virtual bases (undefined behavior if the object isn't actually of the derived class), and `dynamicDowncastFrom<Base>(base)` for the polymorphic ones (returns null if it isn't). Those call the cast functions of the C bindings, and the results are references to the object that was cast.

* `std::function<R(Args...)>` parameters take the `std_function_...` wrapper class, which you construct from a Java lambda with `of(lambda)` (or replace with `assignFn()`). The lambda implements the nested `Fn` interface: it receives scalars and enums by value, exposed structs by value (as copies), and classes by reference, and returns scalars by value or classes by value (an owned wrapper, which the C++ side takes over). The copies of the `std::function` share the lambda. An exception thrown by the lambda is rethrown by the wrapper that called into C, once the C call returns. Callback parameters the generator doesn't understand are raw `MemorySegment`s in `Fn`.

* Functions that the safe layer doesn't understand (`std::shared_ptr`, arrays of unknown bound, ...) are still generated, but take and return raw `MemorySegment`s and primitives. The doc comment of each wrapper names the C function it calls, so the C bindings documentation applies.

Overloaded functions keep their names as long as their Java parameter types differ. Those that would clash get the disambiguated names of their C functions (e.g. `boolean_4_const_MR_Mesh_ref`). Constructors become Java constructors on the same condition, else static factories. Overloaded operators are `opAdd`, `opEqual`, `opNotEqual`, `opAddAssign`, etc.

C++ exceptions are caught by the C bindings (unless the C generator ran with `--no-handle-exceptions`) and rethrown as `CppException` with the exception message.

## Known limitations

This backend is as young as the Rust one, and shares its limitations:

* Inherited members are only available if the C generator copied them (`--copy-inherited-members`). Otherwise upcast to the base explicitly to call its methods, there are no implicit conversions.
* Containers only expose the methods that the C bindings have (`size()`, `at()`, ...), there's no `Iterable` or `List` integration yet.
* Output parameters of scalar types (`int &`) are raw.
* Objects don't keep alive what they reference (the `[[clang::lifetime_capture_by]]` annotations), the doc comments only mention it. A reference parameter that gets stored must outlive the object.
10 changes: 10 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ It parses all C++ headers in `examples/input` and generates the Rust crate in `

Then it runs the test executable at `examples/rust/example_consumer`, which calls into this library.

## Java

You need to have a JDK 22 or newer installed (`javac` and `java` in `PATH`, or `JAVA_HOME` set).

Run `examples/java/run.sh`.

It parses all C++ headers in `examples/input` and generates the Java sources in `examples/java/library/src`, which it compiles into `examples/java/library/example.jar`. It also generates the C bindings required for Java, like the C example, in `examples/java/c_library`.

Then it runs the test program at `examples/java/example_consumer`, which calls into this library.

## Python

Run `examples/python/run.sh`.
Expand Down
6 changes: 6 additions & 0 deletions examples/java/clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

SCRIPT_DIR="$(dirname "$BASH_SOURCE")"
cd "$SCRIPT_DIR/.."

rm -rf java/c_library java/library java/example_consumer/classes
11 changes: 11 additions & 0 deletions examples/java/example_consumer/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import example.example.Functions;

public class Main
{
public static void main(String[] args)
{
Functions.SayHello();
if (Functions.Square(4) != 16)
throw new AssertionError("Square(4) != 16");
}
}
121 changes: 121 additions & 0 deletions examples/java/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/bash

SCRIPT_DIR="$(dirname "$BASH_SOURCE")"
cd "$SCRIPT_DIR/.."

. _detail/set_env_vars.sh

./java/clean.sh

mkdir -p java/c_library/tmp

# Those are the Clang-style flags for the parser.
EXTRA_PARSER_CXX_FLAGS=(
-std=c++20 -Wall -Wextra -pedantic-errors
-fparse-all-comments
)

# Those are optional tunable flags for the parser.
EXTRA_PARSER_FLAGS=(
)

# Those are optional tunable flags for the C generator.
EXTRA_GEN_C_FLAGS=(
--max-header-name-length 100
)

# Those are optional tunable flags for the Java generator.
EXTRA_GEN_FLAGS=(
)

# Those are optional tunable flags for the C bindings compilation.
EXTRA_CXX_FLAGS=(
-g
)

# The JDK tools, from `JAVA_HOME` if it's set. Java 22 or newer is required.
JAVA_BIN="${JAVA_HOME:+$JAVA_HOME/bin/}"

SHARED_LIBRARY_EXT=.so
SHARED_LIBRARY_PREFIX=lib
CLASSPATH_SEP=:

# Need extra flags on MSYS2.
if [[ $(uname -o) == Msys ]]; then
EXTRA_PARSER_CXX_FLAGS+=(--sysroot="$MSYSTEM_PREFIX")
SHARED_LIBRARY_EXT=.dll
SHARED_LIBRARY_PREFIX=
CLASSPATH_SEP=';'
fi

set -x

# Assemble the combined input header.
echo "#pragma once" >java/c_library/tmp/combined_input.h
find input \( -name '*.h' -or -name '*.hpp' \) -printf "#include <%p>\n" >>java/c_library/tmp/combined_input.h

# Parse the input header.
../build/mrbind \
java/c_library/tmp/combined_input.h \
-o java/c_library/tmp/parse_result.json \
--ignore :: \
--allow Example \
--copy-inherited-members \
"${EXTRA_PARSER_FLAGS[@]}" \
-- \
-xc++-header \
-resource-dir="${CLANG_RESOURCE_DIR:-$("$CLANG_CXX" -print-resource-dir)}" \
-I. \
"${EXTRA_PARSER_CXX_FLAGS[@]}"

# Generate the C bindings, required by the Java bindings.
../build/mrbind_gen_c \
--input java/c_library/tmp/parse_result.json \
--output-header-dir java/c_library/include \
--output-source-dir java/c_library/src \
--output-desc-json java/c_library/tmp/c_desc.json \
--helper-name-prefix Example_ \
--helper-macro-name-prefix EXAMPLE_ \
--map-path input . \
--assume-include-dir . \
--force-emit-common-helpers \
"${EXTRA_GEN_C_FLAGS[@]}"

# Generate the Java bindings.
../build/mrbind_gen_java \
--input-json java/c_library/tmp/c_desc.json \
--output-dir java/library/src \
--package example \
--imported-lib-name example \
--force-namespace Example \
"${EXTRA_GEN_FLAGS[@]}"

# Compile the Java bindings into a jar. The list of sources goes through a file, since there can be a lot of them.
find java/library/src -name '*.java' >java/library/sources.txt
"${JAVA_BIN}javac" --release 22 -Xlint:all -Werror -d java/library/classes @java/library/sources.txt
"${JAVA_BIN}jar" --create --file java/library/example.jar -C java/library/classes .

# Find the generated source files of the C bindings.
# Those are C++ sources. Only the generated headers are C.
SOURCES="$(find java/c_library/src -name *.cpp)"

# Compile the C bindings into a library. Using the Java bindings loads this library at runtime.
if [[ $SOURCES ]]; then
# The C bindings can be compiled with any C++ compiler, not necessarily Clang.
"$CLANG_CXX" \
-std=c++20 -Wall -Wextra -pedantic-errors \
-Ijava/c_library/include \
-Ijava/c_library/src \
-I. \
$SOURCES \
-shared -fPIC \
-fvisibility=hidden -fvisibility-inlines-hidden \
-o java/c_library/${SHARED_LIBRARY_PREFIX}example$SHARED_LIBRARY_EXT \
"${EXTRA_CXX_FLAGS[@]}"
else
echo "The generator didn't produce any source files that need to be compiled."
fi

# Compile and run a test program. The C bindings library is found in `java.library.path`.
"${JAVA_BIN}javac" --release 22 -Xlint:all -Werror -cp java/library/example.jar -d java/example_consumer/classes java/example_consumer/Main.java
"${JAVA_BIN}java" --enable-native-access=ALL-UNNAMED -Djava.library.path=java/c_library -cp "java/library/example.jar${CLASSPATH_SEP}java/example_consumer/classes" Main
Loading