Skip to content

Add a Java bindings generator on top of the C bindings - #55

Merged
Fedr merged 10 commits into
vibecodedfrom
java-generator
Oct 15, 2026
Merged

Fedr merged 10 commits into
vibecodedfrom
java-generator

Conversation

@Fedr

@Fedr Fedr commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

A Java backend, generated on top of the C bindings the same way the Rust one (#54) is. Stacked on #54, since it reads the C signatures and the std::function callback types that PR added to the description JSON; rebased on its current head.

What's generated

mrbind_gen_java --input-json c_desc.json --output-dir src --package com.example.mylib --imported-lib-name MyLib [--force-namespace MyLib] emits plain Java sources calling the C library through the foreign function API (java.lang.foreign, Java 22+), so there's no JNI glue and no dependencies:

  • <package>.ffi mirrors the C file hierarchy: one class per C file holding the downcall MethodHandle of every function (in nested classes of 200 handles each, to keep the static initializers under the bytecode limit). A function the library lacks gets a handle that throws UnsatisfiedLinkError when called, so the rest of its class stays usable.
  • The C++ namespaces become packages (MR::Foo → mr.foo), classes become classes, free functions become static methods of a Functions class per package. Opaque classes are CppObject wrappers that own their C++ object when created in Java (destroyed by a Cleaner or close()), and when returned by reference keep alive what the reference points into: the objects named by the [[clang::lifetimebound]] annotations, or this / the only object parameter without them. --expose-as-struct classes are CppStruct views over native memory with the C++ layout, built at runtime with the C alignment rules and checked against the sizes in the JSON. Enums are newtype classes with constants. Parameters take wrappers (nullable for pointers and default arguments), std::string-like ones take String, scalars by const pointer take primitives. C++ exceptions become CppException. Anything the safe layer doesn't understand still gets a wrapper with raw MemorySegments and primitives, so the whole API is reachable.
  • Every class with bases gets upcastTo<Base>(), plus staticDowncastFrom<Base>() for the non-virtual bases and dynamicDowncastFrom<Base>() (null on failure) for the polymorphic ones, calling the cast functions of the C bindings (derived from inheritance_info with the same rules as the C generator).
  • The std_function_... wrappers get of(lambda) and assignFn(lambda), built on ConstructEx/AssignEx through upcall stubs. The lambda implements a nested Fn interface with the natural Java types (scalars, enums, exposed structs by value, classes by reference, classes returned by value as owned wrappers); the copies of the std::function share it through the user data callback; an exception it throws is rethrown by the wrapper that called into C once the call returns.
  • Unlike Rust, Java has overloading, so overloads keep their C++ names when their Java parameter types differ, and only clashing ones get the disambiguated C names. Operators are opAdd, opEqual, opNotEqual, opAddAssign, ... Constructors are real Java constructors when unambiguous.
  • On Windows the library is loaded with LoadLibraryExW(LOAD_WITH_ALTERED_SEARCH_PATH), so its dependencies are found next to it, and the Windows error code is reported when that fails.

See docs/generating_java.md for the details and the known limitations. examples/java has the same structure as examples/rust.

Testing

test/make_java.sh generates the three test outputs, compiles them with -Xlint:all -Werror, and runs test/run_java/Main.java, which exercises opaque classes with string fields, copy constructors, std::vector<int>, enum default arguments, an exposed struct with arrays, a C++ exception turning into a CppException, std::function from lambdas (shared by copies, class passed and returned by value, a lambda exception rethrown by the caller), and the casts. On the full test suite the generator wraps all 4668 functions, 433 of them with raw types, 0 skipped. Verified on Windows (MSYS2 clang64 for the C library, Temurin JDK 25), and examples/java/run.sh runs there too.

On the full MeshLib API (37764 functions, 3157 classes) the generated sources compile in ~20 s with zero warnings; MeshInspector/MeshLib#7115 builds and runs them in CI on Ubuntu, Windows and macOS, x64 and arm64, with all 92 ported tests plus the callback and cast tests passing.

The committed test/output_java* sources are generated on Linux like the C and Rust ones.

🤖 Generated with Claude Code

@adalisk-emikhaylov

Copy link
Copy Markdown
Collaborator

Don't add those comments to generated code: /** The downcall handles of the C bindings file MR/test_abstract_classes. Generated by mrbind, do not edit. */.

Fedr and others added 10 commits October 14, 2026 23:50
Like the Rust generator, it consumes the C bindings description JSON and emits
sources that call the C library through the foreign function API of Java 22,
so there's no JNI glue. Opaque classes are wrappers with cleaner-based
ownership, exposed structs are views over native memory with the C++ layout,
enums are newtype classes, and the rest of the mapping follows the Rust one.
Overloads keep their C++ names when the Java parameter types differ.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each class is a file, and a `std::function` instantiation from MeshLib produced
a 190-character name that overflowed the Windows path length limit. Same
truncation and hash suffix as the C generator applies to header names.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Generated on Linux by test/make_java.sh, which also compiled them and ran the smoke test there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ATH) on Windows

The directory of java.exe comes first in the default DLL search order, and the
JDK ships freetype.dll, which shadowed the real one and made MeshLibC2.dll fail
with "The specified procedure could not be found". With the altered search path
the dependencies are looked up next to the library first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LoadLibraryExW failures were swallowed and only the generic message of the
System.loadLibrary fallback was visible. Also keep the message when the static
initializer of Ffi fails, since the later uses of the class only see a
NoClassDefFoundError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es, no generated-code headers

The same five points as in the Rust backend: the "generated, do not edit"
headers are gone; the [[clang::lifetimebound]] relations choose what a returned
reference keeps alive; every class gets upcastTo<Base>(), and
staticDowncastFrom<Base>() / dynamicDowncastFrom<Base>() for the non-virtual and
polymorphic bases, calling the cast functions of the C bindings; the
std::function wrappers get of(lambda) and assignFn(lambda), built on
ConstructEx/AssignEx through upcall stubs, with the lambda shared by the copies
and its exceptions rethrown by the wrapper that called into C; examples/java is
the same as examples/rust. A class can't be named like a segment of the base
package anymore, the example found that case (example.h in package example).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Fedr
Fedr changed the base branch from rust-generator to vibecoded October 14, 2026 20:53
@Fedr
Fedr merged commit a5bb95b into vibecoded Oct 15, 2026
@Fedr
Fedr deleted the java-generator branch October 15, 2026 11:58
Fedr added a commit to MeshInspector/MeshLib that referenced this pull request Oct 15, 2026
…CI (#7115)

The MeshLib side of MeshInspector/mrbind#55: make -f scripts/mrbind/generate.mk TARGET=java generates source/MeshLibJava/src from the C bindings JSON and compiles meshlib.jar (Java 22+ foreign function API, no JNI). Java examples, MRTestJava (96 tests), a build-java-bindings composite action run on one Release job per OS and architecture (Windows against delvewheel-mangled DLLs, since java.exe already holds its own zip.dll), JavaSetupGuide plus Java tabs in the docs, and meshlib_<version>_java_bindings.zip in the release assets.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants