From 3386da9b84e0602d783fd5b2f39153585a264fc5 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 23 Mar 2026 22:33:01 -0300 Subject: [PATCH 01/12] Add agents --- AGENTS.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..509fdaef --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,112 @@ +# AGENTS.md — jParser + +## Project Overview + +jParser is a Java code-generation and C/C++ compilation library that bridges native code to JVM platforms (desktop, mobile, web). It reads Java source files containing embedded native code blocks, then generates platform-specific Java source for **JNI** (desktop/mobile) and **TeaVM** (web/WASM) targets. It also supports **WebIDL**-driven automatic binding generation. + +### Context Resumption & State Persistence + +If you are asked to continue a task or if this chat has been restored from a backup, **always check for `LOCAL_AGENT.md`** in the project root. + +**This file is the single source of truth for the current state machine.** It must be updated **BEFORE** every significant action. + +### Agent Workflow for Persistence: +1. **Analyze**: Understand the user request and existing codebase. **NEVER guess code structures or property names.** You must verify the existence of classes, methods, and fields by reading the source files. (e.g., Do not assume a variable or a method exists; check the class structure first). +2. **Sync State**: Read `LOCAL_AGENT.md` to understand where the previous session left off. +3. **Plan & Commit**: Document any new architectural decisions, sub-tasks, or file modifications in `LOCAL_AGENT.md` **before** writing code. +4. **After planning:**: Always ask the user if can execute the plan. User may ask questions about the plan. If user approves, proceed to execute. If user asks for changes, update the plan and repeat step 3. +5. **Execute**: Perform file modifications, terminal commands, etc. +6. **Update Progress**: After a task (or significant sub-task) is completed or verified, update the "Current Progress" and "Next Task" sections in `LOCAL_AGENT.md`. +7. **Verify**: Always run compilation to ensure the state is stable before ending the turn. + +This ensures that if the session is interrupted, the next agent has a perfect "snapshot" of the state machine, including why decisions were made and which files were touched. + +## Architecture + +### Core Pipeline (`BuilderTool.build()` in `jParser/jParser-build-tool`) +1. **IDL Parsing** — `IDLReader` reads `.idl` files from `lib-build/src/main/cpp/` +2. **Code Generation (JNI)** — `CppCodeParser` (extends `IDLDefaultCodeParser`) reads `lib-base/src/main/java` source, generates JNI Java into `lib-core/src/main/java` +3. **Code Generation (TeaVM)** — `TeaVMCodeParser` generates TeaVM/JS Java into `lib-teavm/src/main/java` +4. **Native Compilation** — `JBuilder.build()` compiles C/C++ for each platform target via `BuildMultiTarget` + +### Module Layout (follows a strict `-base/-build/-core/-teavm` convention) + +| Suffix | Purpose | Java target | +|---|---|---| +| `lib-base` | Hand-written Java source with embedded `/*[-JNI;-NATIVE]*/` and `/*[-TEAVM;-REPLACE]*/` code blocks | Java 8 | +| `lib-build` | `BuildLib.main()` entry point — configures IDL, targets, runs generation + compilation | Java 11 | +| `lib-core` | **Generated** JNI Java output (do not hand-edit) | Java 11 | +| `lib-teavm` | **Generated** TeaVM Java output (do not hand-edit) | Java 11 | +| `lib-desktop` | Runtime loader for desktop | — | +| `lib-android` | Android-specific packaging | — | + +This pattern repeats across `jParser/`, `idl-helper/`, `loader/`, and `examples/`. + +### Key Modules + +- **`jParser/jParser-core`** — `JParser.generate()` entry point; uses JavaParser to parse/transform Java ASTs. `CodeParser` interface → `DefaultCodeParser` → `IDLDefaultCodeParser`. +- **`jParser/jParser-cpp`** — `CppCodeParser` (header `"JNI"`) generates JNI glue code + `NativeCPPGenerator` emits C++ `.cpp` files. +- **`jParser/jParser-teavm`** — `TeaVMCodeParser` (header `"TEAVM"`) generates `@JSBody`-annotated methods for TeaVM. +- **`jParser/jParser-idl`** — IDL file parser, class model (`IDLClass`, `IDLMethod`, `IDLAttribute`), and code generation parsers. +- **`jParser/jParser-build`** — `JBuilder`, `BuildConfig`, platform targets (`EmscriptenTarget`, `WindowsMSVCTarget`, `LinuxTarget`, etc.). +- **`idl/idl-core`** — `IDLBase` parent class for all native objects (memory management, ownership, dispose). + +## Code Block Convention + +In `lib-base` Java source, native code is embedded via block comments with headers: +```java +/*[-JNI;-NATIVE] + MyType* obj = (MyType*)this_addr; + obj->doSomething(); +*/ +private static native void internal_native_doSomething(long this_addr); + +/*[-TEAVM;-REPLACE] + @org.teavm.jso.JSBody(params = {"this_addr"}, script = "...") + private static native void internal_native_doSomething(int this_addr); +*/ +``` +Commands: `-ADD`, `-ADD_RAW`, `-REMOVE`, `-REPLACE`, `-REPLACE_BLOCK`, `-NATIVE`. Use `-IDL_SKIP` on a class comment to prevent IDL generation for that class. + +## Build & Run + +```sh +# Build everything (from project root) +./gradlew build + +# Generate + compile a specific library (example: TestLib for Windows) +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_windows64 + +# Build all platforms for TestLib +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_all + +# Platform-specific targets: teavm, windows64, linux64, mac64, macArm, android, ios +# These are passed as args to BuildLib.main() +``` + +### Requirements +- **JDK 11+**, **Gradle** (wrapper included) +- **Windows builds**: MinGW64 or Visual Studio C++ (`vcvarsall.bat` must be on PATH) +- **Web builds**: Emscripten SDK (`EMSDK` env var) + +## Conventions + +- **Version management**: `gradle.properties` holds the version; `LibExt.kt` in `buildSrc/` resolves it. Snapshots use `"-SNAPSHOT"`, releases use the property value. +- **Publishing**: The `publish.gradle.kts` plugin configures all library modules listed in `libProjects`. Use `publishRelease` or `publishTestRelease` tasks. +- **Generated code is not hand-edited**: `lib-core/` and `lib-teavm/` directories contain generated output with a "Do not make changes" header. +- **IDL files** live at `lib-build/src/main/cpp/.idl`. Custom C++ glue code goes in `lib-build/src/main/cpp/custom/`. +- **IDLBase** is the parent of all native-bound classes. Memory must be manually managed via `dispose()`. Use `ClassName.NULL` instead of Java `null` for native parameters. +- **Dependencies**: JavaParser (`3.26.1`) for AST manipulation, TeaVM (`0.13.0`) for web target, JUnit 4 for tests. + +## Testing + +```sh +# Run IDL parser tests +./gradlew :jParser:jParser-idl:test + +# Run C++ code parser tests +./gradlew :jParser:jParser-cpp:test +``` + +Test classes live in standard `src/test/java` directories (e.g., `IDLReaderTest`, `CppCodeParserTest`). + From dead91925c56d3b64059895c4210139ddbe7ae79 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 23 Mar 2026 22:57:51 -0300 Subject: [PATCH 02/12] Phase 1 --- FFM_PLAN.md | 473 ++++++++++ LOCAL_AGENT.md | 44 + buildSrc/src/main/kotlin/publish.gradle.kts | 1 + jParser/jParser-build-tool/build.gradle.kts | 1 + jParser/jParser-ffm/build.gradle.kts | 37 + .../xpenatan/jParser/ffm/FFMCodeParser.java | 830 ++++++++++++++++++ .../xpenatan/jParser/ffm/FFMCppGenerator.java | 241 +++++ .../jParser/ffm/FFMMethodHandleRegistry.java | 133 +++ .../jParser/ffm/FFMNativeCodeGenerator.java | 21 + .../xpenatan/jParser/ffm/FFMTypeMapper.java | 107 +++ settings.gradle.kts | 1 + 11 files changed, 1889 insertions(+) create mode 100644 FFM_PLAN.md create mode 100644 LOCAL_AGENT.md create mode 100644 jParser/jParser-ffm/build.gradle.kts create mode 100644 jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java create mode 100644 jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java create mode 100644 jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java create mode 100644 jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java create mode 100644 jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java diff --git a/FFM_PLAN.md b/FFM_PLAN.md new file mode 100644 index 00000000..6b2c5c36 --- /dev/null +++ b/FFM_PLAN.md @@ -0,0 +1,473 @@ +# FFM_PLAN.md — Foreign Function & Memory API Support for jParser + +## Goal + +Add a third code-generation target (alongside JNI and TeaVM) that produces Java classes using the **Foreign Function & Memory (FFM) API** (`java.lang.foreign.*`). FFM eliminates JNI overhead by calling native functions directly through `MethodHandle` downcalls, avoiding JNI parameter marshalling, `JNIEnv*` context, and the `native` keyword entirely. + +The public Java interface (the calling methods) remains **identical** to the JNI path — only the underlying native bridge changes. + +--- + +## Architecture Overview + +### Current Pipeline + +``` +IDL files ──► IDLReader ──► IDLDefaultCodeParser (base class) + │ + ├── CppCodeParser (header "JNI") + │ ├─ Generates: private static native methods + │ ├─ Block comments: [-JNI;-NATIVE] with C++ templates + │ └─ NativeCPPGenerator → JNIGlue.cpp (JNIEXPORT functions) + │ + └── TeaVMCodeParser (header "TEAVM") + ├─ Generates: @JSBody-annotated native methods + └─ Renames to gen.* package +``` + +### Proposed Pipeline (addition) + +``` + ├── FFMCodeParser (header "FFM") + │ ├─ Generates: static MethodHandle fields + private static bridge methods + │ ├─ Block comments: [-FFM;-NATIVE] with C++ templates (no JNI types) + │ └─ FFMCppGenerator → FFMGlue.cpp (extern "C" functions) +``` + +### Why the Same Public Interface Works + +The public methods generated by `IDLMethodParser` / `IDLAttributeParser` / `IDLConstructorParser` call: +```java +internal_native_doSomething(native_address, param1, param2_addr); +``` +These delegate to `private static` methods. In JNI, these are `native` methods resolved by the JVM. In FFM, these become regular static methods that invoke a `MethodHandle`. The calling code does not change. + +--- + +## Implementation Plan + +### Phase 1: Core FFM Module & Code Parser + +#### Task 1.1 — Create `jParser/jParser-ffm` Gradle Module + +Create a new module at `jParser/jParser-ffm/` parallel to `jParser-cpp` and `jParser-teavm`. + +**Files to create:** +- `jParser/jParser-ffm/build.gradle.kts` + +```kotlin +plugins { + id("java-library") +} + +val moduleName = "jParser-ffm" + +dependencies { + implementation(project(":jParser:jParser-idl")) + implementation(project(":jParser:jParser-core")) + implementation(project(":idl:idl-core")) + + testImplementation(project(":loader:loader-core")) + testImplementation("junit:junit:${LibExt.jUnitVersion}") +} + +java { + sourceCompatibility = JavaVersion.toVersion("22") + targetCompatibility = JavaVersion.toVersion("22") +} + +tasks.withType { + options.compilerArgs.addAll(listOf("--enable-preview")) +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + create("maven") { + artifactId = moduleName + group = LibExt.groupId + version = LibExt.libVersion + from(components["java"]) + } + } +} +``` + +**Files to modify:** +- `settings.gradle.kts` — Add `include(":jParser:jParser-ffm")` +- `buildSrc/src/main/kotlin/publish.gradle.kts` — Add `project(":jParser:jParser-ffm")` to `libProjects` +- `jParser/jParser-build-tool/build.gradle.kts` — Add `implementation(project(":jParser:jParser-ffm"))` + +#### Task 1.2 — Implement `FFMCodeParser` + +Create `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java`. + +This class extends `IDLDefaultCodeParser` with header `"FFM"` and overrides the same hooks as `CppCodeParser`: + +| Hook | JNI (CppCodeParser) | FFM (FFMCodeParser) | +|---|---|---| +| `onIDLConstructorGenerated` | `private static native long internal_native_ClassName(...)` + `[-JNI;-NATIVE]` C++ block | `private static long internal_native_ClassName(...)` (non-native) + `MethodHandle` invocation body | +| `onIDLDeConstructorGenerated` | `private static native void internal_native_dispose(long this_addr)` | Same signature but non-native with `MethodHandle` call | +| `onIDLMethodGenerated` | `private static native internal_native_method(long this_addr, ...)` | Non-native method invoking `MethodHandle` | +| `onIDLAttributeGenerated` | `private static native internal_native_get/set_attr(long this_addr, ...)` | Non-native method invoking `MethodHandle` | +| `onIDLEnumMethodGenerated` | `private static native long internal_native_enumValue()` | Non-native method invoking `MethodHandle` | +| `setJavaBodyNativeCMD` | Passes content to `NativeCPPGenerator` | Passes content to `FFMCppGenerator` | +| `parseCodeBlock` (with `CMD_NATIVE`) | Sends C++ to `NativeCPPGenerator` | Sends C++ to `FFMCppGenerator` | + +**Key design decisions:** + +1. **MethodHandle storage**: Each class gets a private inner class `FFMHandles` with static `MethodHandle` fields, one per native function. These are initialized in a static block. + +2. **Bridge method pattern** (what each `internal_native_*` method looks like): +```java +// JNI version (current): +private static native long internal_native_MyClass(int param1); + +// FFM version (new): +private static long internal_native_MyClass(int param1) { + try { + return (long) FFMHandles.internal_native_MyClass.invokeExact(param1); + } catch(Throwable e) { + throw new RuntimeException(e); + } +} +``` + +3. **Static initializer block** (injected in `onParserComplete`): +```java +private static final class FFMHandles { + private static final SymbolLookup LOOKUP; + static { + LOOKUP = SymbolLookup.libraryLookup(System.mapLibraryName("MyLib64"), Arena.global()); + } + + static final MethodHandle internal_native_MyClass = LOOKUP.find("jparser_pkg_MyClass_internal_native_MyClass") + .map(addr -> Linker.nativeLinker().downcallHandle(addr, + FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT))) + .orElseThrow(); + // ... more handles +} +``` + +4. **FFM type mapping** (Java ↔ `ValueLayout`): + +| Java Type | ValueLayout | C Type | +|---|---|---| +| `long` | `JAVA_LONG` | `int64_t` | +| `int` | `JAVA_INT` | `int32_t` | +| `float` | `JAVA_FLOAT` | `float` | +| `double` | `JAVA_DOUBLE` | `double` | +| `boolean` | `JAVA_BOOLEAN` | `int32_t` (0/1) | +| `short` | `JAVA_SHORT` | `int16_t` | +| `byte` | `JAVA_BYTE` | `int8_t` | +| `void` | (none) | `void` | +| `String` | `ADDRESS` | `const char*` (via `Arena.ofAuto()` for allocation) | + +5. **String handling**: JNI automatically converts `jstring` ↔ `char*` via `GetStringUTFChars`/`NewStringUTF`. In FFM: + - **Outgoing strings**: Allocate a native segment with `Arena.ofConfined()`, copy the Java string bytes, pass the address. Free after call. + - **Return strings**: Read from the returned `MemorySegment` using `segment.getString(0)`. + - Generate string marshalling code directly in the Java bridge method body. + +#### Task 1.3 — Implement `FFMCppGenerator` + +Create `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java`. + +Implements the `CppGenerator` interface (or a new parallel interface if decoupling is needed). + +**Key differences from `NativeCPPGenerator`:** + +| Aspect | NativeCPPGenerator (JNI) | FFMCppGenerator (FFM) | +|---|---|---| +| Function signatures | `JNIEXPORT jlong JNICALL Java_com_pkg_Class_method(JNIEnv* env, jclass clazz, jlong this_addr)` | `extern "C" EXPORT int64_t jparser_com_pkg_Class_method(int64_t this_addr)` | +| Types | `jlong`, `jint`, `jfloat`, `jstring`, `jobject` | `int64_t`, `int32_t`, `float`, `const char*`, `void*` | +| String handling | `env->GetStringUTFChars(str, 0)` / `env->ReleaseStringUTFChars(str, ptr)` | Strings arrive as `const char*` directly — no conversion needed | +| Return objects | `return (jlong)ptr;` | `return (int64_t)ptr;` | +| Export macro | `JNIEXPORT` (JNI header) | Custom `EXPORT` macro: `__declspec(dllexport)` on Windows, `__attribute__((visibility("default")))` on Unix | +| Output file | `JNIGlue.cpp` + `JNIGlue.h` | `FFMGlue.cpp` + `FFMGlue.h` | +| Headers needed | `#include ` | `#include ` | + +**Generated C++ template example:** +```cpp +// FFMGlue.h +#pragma once +#include + +#ifdef _WIN32 + #define EXPORT __declspec(dllexport) +#else + #define EXPORT __attribute__((visibility("default"))) +#endif + +extern "C" { + +// Constructor +EXPORT int64_t jparser_com_pkg_MyClass_internal_native_MyClass(int32_t param1) { + return (int64_t)new MyClass((int)param1); +} + +// Method +EXPORT int64_t jparser_com_pkg_MyClass_internal_native_getValue(int64_t this_addr) { + MyClass* nativeObject = (MyClass*)this_addr; + return (int64_t)nativeObject->getValue(); +} + +// Destructor +EXPORT void jparser_com_pkg_MyClass_internal_native_dispose(int64_t this_addr) { + MyClass* nativeObject = (MyClass*)this_addr; + delete nativeObject; +} + +} // extern "C" +``` + +**Function naming convention:** `jparser___[__]` + +This differs from JNI naming (`Java_com_pkg_Class_method__JI`) but follows a similar pattern. The names must match exactly between: +- The C++ function name in `FFMGlue.cpp` +- The `SymbolLookup.find("...")` string in the generated Java code + +#### Task 1.4 — Implement `FFMMethodHandleRegistry` + +Create a utility class that `FFMCodeParser` uses during `onParserComplete` to inject the `FFMHandles` inner class: + +`jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java` + +This tracks all native functions discovered during parsing and, at the end, generates: +1. The `FFMHandles` inner class per Java class +2. The correct `FunctionDescriptor` for each method +3. The `SymbolLookup` initialization + +The registry accumulates entries like: +```java +class FFMEntry { + String symbolName; // "jparser_com_pkg_Class_method" + String javaMethodName; // "internal_native_method" + Type returnType; // from JavaParser AST + List parameterTypes; // from JavaParser AST + boolean isStatic; +} +``` + +### Phase 2: Build Integration + +#### Task 2.1 — Extend `BuildToolOptions` + +In `jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java`: + +- Add `public boolean generateFFM = false;` +- Add `private String moduleFFMPath;` computed as `modulePath + "/" + modulePrefix + "-ffm"` +- Add getter `getModuleFFMPath()` +- In `setup()`, compute `moduleFFMPath` like `moduleCorePath` and `moduleTeavmPath` + +#### Task 2.2 — Extend `BuilderTool` + +In `jParser/jParser-build-tool/src/main/java/com/github/xpenatan/jParser/builder/tool/BuilderTool.java`: + +Add a third generation block in `generateAndBuild()`: + +```java +if(op.generateFFM) { + FFMCppGenerator ffmGenerator = new FFMCppGenerator(op.getCPPDestinationPath()); + FFMCodeParser ffmParser = new FFMCodeParser(ffmGenerator, idlReader, op.packageName, op.getSourceDir()); + ffmParser.generateClass = true; + ffmParser.idlRenaming = packageRenaming; + JParser.generate(ffmParser, op.getModuleBaseJavaDir(), op.getModuleFFMPath() + "/src/main/java"); +} +``` + +#### Task 2.3 — FFM-Specific Build Targets + +The C++ compilation for FFM differs from JNI: + +- **No JNI headers required** — No `addJNIHeaders()` call +- **Different glue code** — Links `FFMGlue.cpp` instead of `JNIGlue.cpp` +- **Same shared library output** — `.dll` / `.so` / `.dylib` (same as JNI) +- **Exported symbols** — Must use `extern "C"` + visibility attributes + +Create convenience method in `DefaultBuildTarget`: +```java +public void addFFMGlueCode(String libBuildCPPPath) { + cppInclude.add(libBuildCPPPath + "/src/ffmglue/FFMGlue.cpp"); +} +``` + +### Phase 3: Runtime & Downstream Module Convention + +#### Task 3.1 — Library Loader for FFM + +Create `loader/loader-ffm/` module with `FFMLibraryLoader.java`: + +```java +public class FFMLibraryLoader { + private static SymbolLookup lookup; + + public static SymbolLookup load(String libraryName, String path) { + String fullPath = path + "/" + System.mapLibraryName(libraryName + "64"); + lookup = SymbolLookup.libraryLookup(Path.of(fullPath), Arena.global()); + return lookup; + } + + public static SymbolLookup getLookup() { + return lookup; + } +} +``` + +Alternatively, keep loading simple and let each generated class handle its own `SymbolLookup` — but a shared loader avoids loading the library multiple times. + +**Recommended approach**: A single `SymbolLookup` per library, initialized by the loader, and referenced by all generated classes. + +#### Task 3.2 — Downstream `lib-ffm` Module Convention + +For each library using jParser (e.g., `examples/TestLib`): + +``` +lib/ + lib-base/ (hand-written source with code blocks - unchanged) + lib-build/ (BuildLib.java - add FFM generation flag) + lib-core/ (generated JNI Java - unchanged) + lib-teavm/ (generated TeaVM Java - unchanged) + lib-ffm/ (NEW: generated FFM Java) + lib-desktop/ (runtime loader + packaged native libs - reused for FFM) +``` + +The `lib-ffm` module: +- Depends on `idl-core` (for `IDLBase`) +- Depends on `loader-ffm` (or `loader-core` if extending it) +- Contains generated Java with `MethodHandle`-based bridge methods +- Java 22+ target + +#### Task 3.3 — Example: Update `TestLib` BuildLib + +In `examples/TestLib/lib/lib-build/src/main/java/BuildLib.java`, add FFM handling: + +```java +if(op.containsArg("ffm")) { + op.generateFFM = true; + // FFM reuses the same native compilation targets but with FFMGlue +} +``` + +### Phase 4: Advanced Features (Deferred) + +#### Task 4.1 — Callback Support via `upcallStub` + +JNI callbacks use `env->CallVoidMethod(obj, methodID, ...)` to call back into Java. FFM requires: + +```java +MethodHandle target = MethodHandles.lookup().findVirtual(CallbackClass.class, "onEvent", ...); +MemorySegment upcallStub = Linker.nativeLinker().upcallStub(target, descriptor, arena); +// Pass upcallStub.address() to C++ as a function pointer +``` + +**This is architecturally different from JNI callbacks** and requires: +- Generating `upcallStub` creation code +- The C++ callback class must call a function pointer instead of `env->CallVoidMethod` +- Lifecycle management of the `Arena` backing the upcall stub + +**Recommendation**: Defer callbacks to Phase 4. Most FFM performance gains come from method calls, not callbacks. + +#### Task 4.2 — Array/Buffer Optimization + +FFM can avoid copying array data between Java and native: +- `MemorySegment.ofArray(array)` gives direct access to Java array memory +- `MemorySegment.ofBuffer(byteBuffer)` for direct buffers + +This is a performance optimization opportunity beyond what JNI provides (JNI requires `GetArrayElements` / `ReleaseArrayElements`). + +#### Task 4.3 — `[-FFM;-NATIVE]` Code Block Support + +In `lib-base` source, users can embed FFM-specific code blocks: + +```java +/*[-FFM;-NATIVE] + // Custom FFM-specific bridge code +*/ +``` + +`FFMCodeParser.setJavaBodyNativeCMD()` would handle this, similar to how `CppCodeParser` handles `[-JNI;-NATIVE]`. + +--- + +## File Change Summary + +### New Files + +| File | Purpose | +|---|---| +| `jParser/jParser-ffm/build.gradle.kts` | Module build config (Java 22+) | +| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java` | Main FFM code parser | +| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java` | C glue code generator (extern "C") | +| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java` | Tracks MethodHandle entries per class | +| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java` | Maps Java types to ValueLayout and C types | +| `jParser/jParser-ffm/src/test/java/com/github/xpenatan/jParser/ffm/FFMCodeParserTest.java` | Unit tests | +| `loader/loader-ffm/build.gradle.kts` | FFM library loader module | +| `loader/loader-ffm/src/main/java/com/github/xpenatan/jParser/loader/FFMLibraryLoader.java` | Runtime loader using SymbolLookup | + +### Modified Files + +| File | Change | +|---|---| +| `settings.gradle.kts` | Add `include(":jParser:jParser-ffm")` and `include(":loader:loader-ffm")` | +| `buildSrc/src/main/kotlin/publish.gradle.kts` | Add both new modules to `libProjects` | +| `jParser/jParser-build-tool/build.gradle.kts` | Add dependency on `jParser-ffm` | +| `jParser/jParser-build-tool/src/main/java/.../BuilderTool.java` | Add FFM generation block | +| `jParser/jParser-build/src/main/java/.../tool/BuildToolOptions.java` | Add `generateFFM`, `moduleFFMPath` | +| `jParser/jParser-build/src/main/java/.../DefaultBuildTarget.java` | Add `addFFMGlueCode()` helper | + +### Downstream Library Changes (per library) + +| File | Change | +|---|---| +| `lib/lib-build/build.gradle.kts` | Add FFM-specific Gradle tasks | +| `lib/lib-build/src/main/java/BuildLib.java` | Add FFM target handling | +| `lib/lib-ffm/build.gradle.kts` | New module (generated output, Java 22+) | + +--- + +## Execution Order + +``` +Phase 1 (Core - do first): + 1.1 Create jParser-ffm module skeleton + 1.2 Implement FFMCodeParser + 1.3 Implement FFMCppGenerator + 1.4 Implement FFMMethodHandleRegistry + FFMTypeMapper + +Phase 2 (Build integration): + 2.1 Extend BuildToolOptions + 2.2 Extend BuilderTool + 2.3 Add FFM build target helpers + +Phase 3 (Runtime & examples): + 3.1 Create loader-ffm module + 3.2 Establish lib-ffm convention + 3.3 Update TestLib example + +Phase 4 (Advanced - deferred): + 4.1 Callback support via upcallStub + 4.2 Array/Buffer optimization + 4.3 [-FFM;-NATIVE] code block support +``` + +--- + +## Key Design Decisions + +1. **Java version**: Target Java 24 where FFM is stable (no `--enable-preview` needed). The `jParser-ffm` module has a higher Java target than the rest of the project (Java 11). + +2. **Coexistence**: `lib-core` (JNI) and `lib-ffm` (FFM) exist side-by-side. Downstream projects choose which to depend on. The public API is identical — only the internal bridge differs. This means a user's application code (`app/core`) can switch from JNI to FFM by changing a single dependency. + +3. **Symbol naming**: Use `jparser___` convention for C function names. This avoids JNI's complex mangling and is easier to debug. The naming must be deterministic and match between `FFMCppGenerator` and `FFMCodeParser`. + +4. **No `native` keyword**: FFM methods are regular static Java methods. The `native` keyword is not used. This means `DefaultCodeParser.CMD_NATIVE` handling needs a slight adaptation — for FFM, when a `[-FFM;-NATIVE]` block is encountered on a method, the C++ code is collected by `FFMCppGenerator` but the Java method gets a `MethodHandle.invokeExact()` body instead of being marked `native`. + +5. **Library loading strategy**: Single `SymbolLookup` per native library, stored in a generated utility class or provided by `loader-ffm`. All `MethodHandle` fields reference this shared lookup. The lookup is created once when the library is first loaded. + +6. **Error handling**: `MethodHandle.invokeExact()` throws checked `Throwable`. Each bridge method wraps this in an unchecked `RuntimeException`. This matches JNI behavior where native crashes propagate as runtime exceptions. + + diff --git a/LOCAL_AGENT.md b/LOCAL_AGENT.md new file mode 100644 index 00000000..bf251dfb --- /dev/null +++ b/LOCAL_AGENT.md @@ -0,0 +1,44 @@ +# LOCAL_AGENT.md — Session State + +## Current Task +FFM (Foreign Function & Memory API) — Phase 1 implementation complete. + +## Current Progress +- [x] Deep codebase analysis: traced full JNI and TeaVM code generation pipelines +- [x] Created `FFM_PLAN.md` with 4-phase implementation plan +- [x] **Task 1.1**: Created `jParser/jParser-ffm` Gradle module (Java 11 build-time, generates Java 24 code) +- [x] **Task 1.1**: Registered in `settings.gradle.kts`, `publish.gradle.kts`, `jParser-build-tool/build.gradle.kts` +- [x] **Task 1.2**: Implemented `FFMCodeParser` extending `IDLDefaultCodeParser` with header `"FFM"` +- [x] **Task 1.3**: Implemented `FFMCppGenerator` — emits `extern "C"` functions with `int64_t`/`int32_t` types +- [x] **Task 1.4**: Implemented `FFMMethodHandleRegistry` — tracks MethodHandle entries per class +- [x] **Task 1.4**: Implemented `FFMTypeMapper` — maps Java types to ValueLayout/C types +- [x] Created `FFMNativeCodeGenerator` interface (decoupled from jParser-cpp CppGenerator) +- [x] Compilation verified: `jParser-ffm` and `jParser-build-tool` both compile successfully +- [x] Existing tests pass (jParser-idl:test) + +## Next Task +Phase 2 implementation: +1. Extend `BuildToolOptions` with `generateFFM` flag +2. Extend `BuilderTool.generateAndBuild()` with FFM generation block +3. Add FFM build target helpers + +## Files Created +- `jParser/jParser-ffm/build.gradle.kts` +- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java` +- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java` +- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java` +- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java` +- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java` + +## Files Modified +- `settings.gradle.kts` — Added `:jParser:jParser-ffm` +- `buildSrc/src/main/kotlin/publish.gradle.kts` — Added to `libProjects` +- `jParser/jParser-build-tool/build.gradle.kts` — Added dependency on `jParser-ffm` + +## Key Design Decisions +- `jParser-ffm` module compiles on Java 11 (it's a build-time code generator); the **generated output** targets Java 24 +- Created `FFMNativeCodeGenerator` interface instead of depending on `jParser-cpp`'s `CppGenerator` (decoupled) +- C++ templates use `int64_t`/`int32_t` casts instead of `jlong`/`jint` +- Symbol naming: `jparser_____` +- FFM bridge methods use `MethodHandle.invokeExact()` wrapped in try/catch +- `SymbolLookup.loaderLookup()` used for library resolution (relies on `System.loadLibrary`) diff --git a/buildSrc/src/main/kotlin/publish.gradle.kts b/buildSrc/src/main/kotlin/publish.gradle.kts index fa09c4dc..9b209cd4 100644 --- a/buildSrc/src/main/kotlin/publish.gradle.kts +++ b/buildSrc/src/main/kotlin/publish.gradle.kts @@ -10,6 +10,7 @@ var libProjects = mutableSetOf( project(":jParser:jParser-idl"), project(":jParser:jParser-cpp"), project(":jParser:jParser-teavm"), + project(":jParser:jParser-ffm"), project(":idl:idl-core"), project(":idl:idl-teavm"), project(":idl-helper:idl-helper-base"), diff --git a/jParser/jParser-build-tool/build.gradle.kts b/jParser/jParser-build-tool/build.gradle.kts index 359a9526..c6eb5936 100644 --- a/jParser/jParser-build-tool/build.gradle.kts +++ b/jParser/jParser-build-tool/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { implementation(project(":jParser:jParser-idl")) implementation(project(":jParser:jParser-teavm")) implementation(project(":jParser:jParser-cpp")) + implementation(project(":jParser:jParser-ffm")) implementation(project(":jParser:jParser-build")) } diff --git a/jParser/jParser-ffm/build.gradle.kts b/jParser/jParser-ffm/build.gradle.kts new file mode 100644 index 00000000..db2e0863 --- /dev/null +++ b/jParser/jParser-ffm/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("java-library") +} + +val moduleName = "jParser-ffm" + +dependencies { + implementation(project(":jParser:jParser-idl")) + implementation(project(":jParser:jParser-core")) + implementation(project(":idl:idl-core")) + + testImplementation(project(":loader:loader-core")) + testImplementation("junit:junit:${LibExt.jUnitVersion}") +} + +java { + sourceCompatibility = JavaVersion.toVersion(LibExt.java11Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java11Target) +} + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + create("maven") { + artifactId = moduleName + group = LibExt.groupId + version = LibExt.libVersion + from(components["java"]) + } + } +} + + diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java new file mode 100644 index 00000000..c47f754a --- /dev/null +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java @@ -0,0 +1,830 @@ +package com.github.xpenatan.jParser.ffm; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Modifier; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.InitializerDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.body.VariableDeclarator; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.stmt.BlockStmt; +import com.github.javaparser.ast.type.Type; +import com.github.javaparser.utils.Pair; +import com.github.xpenatan.jParser.core.JParser; +import com.github.xpenatan.jParser.core.JParserHelper; +import com.github.xpenatan.jParser.core.JParserItem; +import com.github.xpenatan.jParser.idl.IDLAttribute; +import com.github.xpenatan.jParser.idl.IDLClass; +import com.github.xpenatan.jParser.idl.IDLConstructor; +import com.github.xpenatan.jParser.idl.IDLEnumClass; +import com.github.xpenatan.jParser.idl.IDLEnumItem; +import com.github.xpenatan.jParser.idl.IDLFile; +import com.github.xpenatan.jParser.idl.IDLHelper; +import com.github.xpenatan.jParser.idl.IDLMethod; +import com.github.xpenatan.jParser.idl.IDLParameter; +import com.github.xpenatan.jParser.idl.IDLReader; +import com.github.xpenatan.jParser.idl.parser.IDLAttributeOperation; +import com.github.xpenatan.jParser.idl.parser.IDLDefaultCodeParser; +import com.github.xpenatan.jParser.idl.parser.IDLMethodOperation; +import com.github.xpenatan.jParser.idl.parser.IDLMethodParser; +import com.github.xpenatan.jParser.idl.parser.data.IDLParameterData; +import java.util.ArrayList; +import java.util.List; + +/** + * FFM code parser that generates Java classes using java.lang.foreign MethodHandle downcalls + * instead of JNI native methods. Parallel to CppCodeParser. + * + *

For each native method, generates: + *

    + *
  • A private static (non-native) bridge method that invokes a MethodHandle
  • + *
  • C++ glue code using extern "C" with standard C types (via FFMCppGenerator)
  • + *
+ */ +public class FFMCodeParser extends IDLDefaultCodeParser { + + private static final String HEADER_CMD = "FFM"; + + // Same template tags as CppCodeParser (the C++ code is largely the same) + protected static final String TEMPLATE_TAG_TYPE = "[TYPE]"; + protected static final String TEMPLATE_TAG_METHOD = "[METHOD]"; + protected static final String TEMPLATE_TAG_OPERATOR = "[OPERATOR]"; + protected static final String TEMPLATE_TAG_ATTRIBUTE = "[ATTRIBUTE]"; + protected static final String TEMPLATE_TAG_ENUM = "[ENUM]"; + protected static final String TEMPLATE_TAG_ATTRIBUTE_TYPE = "[ATTRIBUTE_TYPE]"; + protected static final String TEMPLATE_TAG_RETURN_TYPE = "[RETURN_TYPE]"; + protected static final String TEMPLATE_TAG_CONST = "[CONST]"; + protected static final String TEMPLATE_TAG_COPY_TYPE = "[COPY_TYPE]"; + protected static final String TEMPLATE_TAG_COPY_PARAM = "[COPY_PARAM]"; + protected static final String TEMPLATE_TAG_CONSTRUCTOR = "[CONSTRUCTOR]"; + protected static final String TEMPLATE_TAG_CAST = "[CAST]"; + + // C++ templates — identical to CppCodeParser but with int64_t casts instead of jlong + protected static final String GET_CONSTRUCTOR_OBJ_POINTER_TEMPLATE = + "\nreturn (int64_t)new [CONSTRUCTOR];\n"; + + protected static final String METHOD_DELETE_OBJ_POINTER_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "delete nativeObject;\n"; + + // --- Attribute templates (int64_t instead of jlong, int32_t instead of jint) --- + + protected static final String ATTRIBUTE_SET_PRIMITIVE_STATIC_TEMPLATE = + "\n[TYPE]::[ATTRIBUTE] = [ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_ARRAY_SET_PRIMITIVE_STATIC_TEMPLATE = + "\n[TYPE]::[ATTRIBUTE][index] = [ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_SET_PRIMITIVE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "nativeObject->[ATTRIBUTE] = [CAST][ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_ARRAY_SET_PRIMITIVE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "nativeObject->[ATTRIBUTE][index] = [CAST][ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_SET_OBJECT_POINTER_STATIC_TEMPLATE = + "\n[TYPE]::[ATTRIBUTE] = ([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr;\n"; + + protected static final String ATTRIBUTE_ARRAY_SET_OBJECT_POINTER_STATIC_TEMPLATE = + "\n[TYPE]::[ATTRIBUTE][index] = ([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr;\n"; + + protected static final String ATTRIBUTE_SET_OBJECT_POINTER_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "nativeObject->[ATTRIBUTE] = ([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr;\n"; + + protected static final String ATTRIBUTE_ARRAY_SET_OBJECT_POINTER_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "nativeObject->[ATTRIBUTE][index] = ([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr;\n"; + + protected static final String ATTRIBUTE_SET_OBJECT_VALUE_STATIC_TEMPLATE = + "\n[TYPE]::[ATTRIBUTE] = *(([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr);\n"; + + protected static final String ATTRIBUTE_ARRAY_SET_OBJECT_VALUE_STATIC_TEMPLATE = + "\n[TYPE]::[ATTRIBUTE][index] = *(([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr);\n"; + + protected static final String ATTRIBUTE_SET_OBJECT_VALUE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "nativeObject->[ATTRIBUTE] = *(([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr);\n"; + + protected static final String ATTRIBUTE_ARRAY_SET_OBJECT_VALUE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "nativeObject->[ATTRIBUTE][index] = *(([ATTRIBUTE_TYPE]*)[ATTRIBUTE]_addr);\n"; + + protected static final String ATTRIBUTE_GET_OBJECT_VALUE_STATIC_TEMPLATE = + "\nreturn (int64_t)&[TYPE]::[ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_ARRAY_GET_OBJECT_VALUE_STATIC_TEMPLATE = + "\nreturn (int64_t)&[TYPE]::[ATTRIBUTE][index];\n"; + + protected static final String ATTRIBUTE_GET_OBJECT_VALUE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return (int64_t)&nativeObject->[ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_ARRAY_GET_OBJECT_VALUE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return (int64_t)&nativeObject->[ATTRIBUTE][index];\n"; + + protected static final String ATTRIBUTE_GET_OBJECT_POINTER_STATIC_TEMPLATE = + "\nreturn (int64_t)[TYPE]::[ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_ARRAY_GET_OBJECT_POINTER_STATIC_TEMPLATE = + "\nreturn (int64_t)([TYPE]::[ATTRIBUTE][index]);\n"; + + protected static final String ATTRIBUTE_GET_OBJECT_POINTER_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "[CONST][ATTRIBUTE_TYPE]* attr = nativeObject->[ATTRIBUTE];\n" + + "return (int64_t)attr;\n"; + + protected static final String ATTRIBUTE_ARRAY_GET_OBJECT_POINTER_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "[CONST][ATTRIBUTE_TYPE]* attr = (nativeObject->[ATTRIBUTE][index]);\n" + + "return (int64_t)attr;\n"; + + protected static final String ATTRIBUTE_GET_PRIMITIVE_STATIC_TEMPLATE = + "\nreturn [TYPE]::[ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_ARRAY_GET_PRIMITIVE_STATIC_TEMPLATE = + "\nreturn [TYPE]::[ATTRIBUTE][index];\n"; + + protected static final String ATTRIBUTE_GET_PRIMITIVE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return [CAST]nativeObject->[ATTRIBUTE];\n"; + + protected static final String ATTRIBUTE_ARRAY_GET_PRIMITIVE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return [CAST]nativeObject->[ATTRIBUTE][index];\n"; + + // --- Method templates --- + + protected static final String METHOD_GET_OBJ_VALUE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "static [COPY_TYPE] [COPY_PARAM];\n" + + "[COPY_PARAM] = nativeObject->[METHOD];\n" + + "return (int64_t)&[COPY_PARAM];"; + + protected static final String METHOD_GET_OBJ_VALUE_ARITHMETIC_OPERATOR_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "static [COPY_TYPE] [COPY_PARAM];\n" + + "[COPY_PARAM] = [OPERATOR];\n" + + "return (int64_t)&[COPY_PARAM];"; + + protected static final String METHOD_GET_OBJ_VALUE_STATIC_TEMPLATE = + "\nstatic [COPY_TYPE] [COPY_PARAM];\n" + + "[COPY_PARAM] = [TYPE]::[METHOD];\n" + + "return (int64_t)&[COPY_PARAM];"; + + protected static final String METHOD_CALL_VOID_STATIC_TEMPLATE = + "\n[TYPE]::[METHOD];\n"; + + protected static final String METHOD_CALL_VOID_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "nativeObject->[METHOD];\n"; + + protected static final String METHOD_GET_OBJ_POINTER_STATIC_TEMPLATE = + "\nreturn (int64_t)[TYPE]::[METHOD];\n"; + + protected static final String METHOD_GET_OBJ_POINTER_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "[CONST][RETURN_TYPE]* obj = nativeObject->[METHOD];\n" + + "return (int64_t)obj;\n"; + + protected static final String METHOD_GET_OBJ_POINTER_OPERATOR_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "[CONST][RETURN_TYPE]* obj = [OPERATOR];\n" + + "return (int64_t)obj;\n"; + + protected static final String METHOD_GET_REF_OBJ_POINTER_STATIC_TEMPLATE = + "\nreturn (int64_t)&[TYPE]::[METHOD];\n"; + + protected static final String METHOD_GET_REF_OBJ_POINTER_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return (int64_t)&nativeObject->[METHOD];\n"; + + protected static final String METHOD_GET_REF_OBJ_POINTER_OPERATOR_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return (int64_t)&[OPERATOR];\n"; + + protected static final String METHOD_GET_PRIMITIVE_STATIC_TEMPLATE = + "\nreturn [CAST][TYPE]::[METHOD];\n"; + + protected static final String METHOD_GET_PRIMITIVE_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return [CAST]nativeObject->[METHOD];\n"; + + protected static final String METHOD_GET_PRIMITIVE_OPERATOR_TEMPLATE = + "\n[TYPE]* nativeObject = ([TYPE]*)this_addr;\n" + + "return ([OPERATOR]);"; + + protected static final String ENUM_GET_INT_TEMPLATE = + "\nreturn (int64_t)[ENUM];\n"; + + private final FFMNativeCodeGenerator cppGenerator; + private final FFMMethodHandleRegistry registry = new FFMMethodHandleRegistry(); + + public FFMCodeParser(FFMNativeCodeGenerator cppGenerator, String cppDir) { + this(cppGenerator, null, "", cppDir); + } + + public FFMCodeParser(FFMNativeCodeGenerator cppGenerator, IDLReader idlReader, String basePackage, String cppDir) { + super(basePackage, HEADER_CMD, idlReader, cppDir); + this.cppGenerator = cppGenerator; + } + + // ==================== IDL Generation Hooks ==================== + + @Override + public void onIDLConstructorGenerated(JParser jParser, IDLConstructor idlConstructor, + ClassOrInterfaceDeclaration classDeclaration, + ConstructorDeclaration constructorDeclaration, + MethodDeclaration nativeMethodDeclaration) { + IDLClass idlClass = idlConstructor.idlClass; + String classTypeName = idlClass.getCPPName(); + + NodeList parameters = constructorDeclaration.getParameters(); + ArrayList idParameters = idlConstructor.parameters; + String params = getParams(parameters, idParameters); + + String constructor = classTypeName + "(" + params + ")"; + String content = GET_CONSTRUCTOR_OBJ_POINTER_TEMPLATE.replace(TEMPLATE_TAG_CONSTRUCTOR, constructor); + + String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]"; + String blockComment = header + content; + nativeMethodDeclaration.setBlockComment(blockComment); + } + + @Override + public void onIDLDeConstructorGenerated(JParser jParser, IDLClass idlClass, + ClassOrInterfaceDeclaration classDeclaration, + MethodDeclaration nativeMethodDeclaration) { + String classTypeName; + if(idlClass.callbackImpl == null) { + classTypeName = idlClass.getCPPName(); + } + else { + classTypeName = idlClass.callbackImpl.name; + } + + String content = METHOD_DELETE_OBJ_POINTER_TEMPLATE.replace(TEMPLATE_TAG_TYPE, classTypeName); + + String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]"; + String blockComment = header + content; + nativeMethodDeclaration.setBlockComment(blockComment); + } + + @Override + public void onIDLMethodGenerated(JParser jParser, IDLMethod idlMethod, + ClassOrInterfaceDeclaration classDeclaration, + MethodDeclaration methodDeclaration, + MethodDeclaration nativeMethodDeclaration) { + String param = getParams(idlMethod, methodDeclaration); + setupMethodGenerated(idlMethod, param, classDeclaration, methodDeclaration, nativeMethodDeclaration); + } + + @Override + public void onIDLAttributeGenerated(JParser jParser, IDLAttribute idlAttribute, boolean isSet, + ClassOrInterfaceDeclaration classDeclaration, + MethodDeclaration methodDeclaration, + MethodDeclaration nativeMethod) { + String attributeName = idlAttribute.name; + String classTypeName = classDeclaration.getNameAsString(); + IDLClass idlClass = idlAttribute.idlFile.getClass(classTypeName); + if(idlClass != null) { + classTypeName = idlClass.getCPPName(); + } + + String getPrimitiveCast = ""; + String attributeType = idlAttribute.getCPPType(); + String constTag = ""; + if(idlAttribute.isConst) { + constTag = "const "; + } + + IDLClass retTypeClass = idlAttribute.idlFile.getClass(attributeType); + if(retTypeClass != null) { + attributeType = retTypeClass.getCPPName(); + } + + if(idlAttribute.isAny) { + getPrimitiveCast = "(int64_t)"; + } + + String attributeReturnCast = ""; + + IDLEnumClass idlEnum = idlAttribute.idlFile.getEnum(attributeType); + if(idlEnum != null) { + if(idlEnum.typePrefix.equals(attributeType)) { + attributeReturnCast = "(" + attributeType + ")"; + } + else { + attributeReturnCast = "(" + idlEnum.typePrefix + "::" + attributeType + ")"; + } + getPrimitiveCast = "(int32_t)"; + } + + String content = null; + IDLAttributeOperation.Op op = IDLAttributeOperation.getEnum(isSet, idlAttribute, methodDeclaration, nativeMethod); + switch(op) { + case SET_OBJECT_VALUE: + content = ATTRIBUTE_SET_OBJECT_VALUE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_ARRAY_OBJECT_VALUE: + content = ATTRIBUTE_ARRAY_SET_OBJECT_VALUE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_OBJECT_VALUE_STATIC: + content = ATTRIBUTE_SET_OBJECT_VALUE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_ARRAY_OBJECT_VALUE_STATIC: + content = ATTRIBUTE_ARRAY_SET_OBJECT_VALUE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_OBJECT_VALUE: + content = ATTRIBUTE_GET_OBJECT_VALUE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_ARRAY_OBJECT_VALUE: + content = ATTRIBUTE_ARRAY_GET_OBJECT_VALUE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_OBJECT_VALUE_STATIC: + content = ATTRIBUTE_GET_OBJECT_VALUE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_ARRAY_OBJECT_VALUE_STATIC: + content = ATTRIBUTE_ARRAY_GET_OBJECT_VALUE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_OBJECT_POINTER: + content = ATTRIBUTE_SET_OBJECT_POINTER_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_ARRAY_OBJECT_POINTER: + content = ATTRIBUTE_ARRAY_SET_OBJECT_POINTER_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_OBJECT_POINTER_STATIC: + content = ATTRIBUTE_SET_OBJECT_POINTER_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_ARRAY_OBJECT_POINTER_STATIC: + content = ATTRIBUTE_ARRAY_SET_OBJECT_POINTER_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_OBJECT_POINTER: + content = ATTRIBUTE_GET_OBJECT_POINTER_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_CONST, constTag); + break; + case GET_ARRAY_OBJECT_POINTER: + content = ATTRIBUTE_ARRAY_GET_OBJECT_POINTER_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_ATTRIBUTE_TYPE, attributeType).replace(TEMPLATE_TAG_CONST, constTag); + break; + case GET_OBJECT_POINTER_STATIC: + content = ATTRIBUTE_GET_OBJECT_POINTER_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_ARRAY_OBJECT_POINTER_STATIC: + content = ATTRIBUTE_ARRAY_GET_OBJECT_POINTER_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_PRIMITIVE: + content = ATTRIBUTE_SET_PRIMITIVE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_CAST, attributeReturnCast); + break; + case SET_PRIMITIVE_STATIC: + content = ATTRIBUTE_SET_PRIMITIVE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_ARRAY_PRIMITIVE_STATIC: + content = ATTRIBUTE_ARRAY_SET_PRIMITIVE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case SET_ARRAY_PRIMITIVE: + content = ATTRIBUTE_ARRAY_SET_PRIMITIVE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_CAST, attributeReturnCast); + break; + case GET_PRIMITIVE: + content = ATTRIBUTE_GET_PRIMITIVE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_CAST, getPrimitiveCast); + break; + case GET_ARRAY_PRIMITIVE: + content = ATTRIBUTE_ARRAY_GET_PRIMITIVE_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_CAST, getPrimitiveCast); + break; + case GET_PRIMITIVE_STATIC: + content = ATTRIBUTE_GET_PRIMITIVE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_ARRAY_PRIMITIVE_STATIC: + content = ATTRIBUTE_ARRAY_GET_PRIMITIVE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_ATTRIBUTE, attributeName).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + } + + if(content != null) { + String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]"; + String blockComment = header + content; + nativeMethod.setBlockComment(blockComment); + } + } + + @Override + public void onIDLEnumMethodGenerated(JParser jParser, IDLEnumClass idlEnum, + EnumDeclaration enumDeclaration, + IDLEnumItem enumItem, + MethodDeclaration nativeMethodDeclaration) { + String enumStr = enumItem.name; + String content = ENUM_GET_INT_TEMPLATE.replace(TEMPLATE_TAG_ENUM, enumStr); + String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]"; + String blockComment = header + content; + nativeMethodDeclaration.setBlockComment(blockComment); + } + + // ==================== Code Block Parsing ==================== + + @Override + public boolean parseCodeBlock(Node node, String headerCommands, String content) { + if(!super.parseCodeBlock(node, headerCommands, content)) { + if(headerCommands.contains(CMD_NATIVE)) { + cppGenerator.addNativeCode(node, content); + return true; + } + } + return false; + } + + @Override + protected void setJavaBodyNativeCMD(String content, MethodDeclaration methodDeclaration) { + // Collect C++ code for the FFM glue file + cppGenerator.addNativeCode(methodDeclaration, content); + + // Register the MethodHandle entry for this native method + registerNativeMethod(methodDeclaration); + + // Transform the native method into an FFM bridge method + convertToFFMBridgeMethod(methodDeclaration); + } + + // ==================== Lifecycle Hooks ==================== + + @Override + public void onParseClassStart(JParser jParser, CompilationUnit unit, TypeDeclaration classOrEnum) { + String nameAsString = classOrEnum.getNameAsString(); + String include = classCppPath.get(nameAsString); + super.onParseClassStart(jParser, unit, classOrEnum); + } + + @Override + public void onParseFileEnd(JParser jParser, JParserItem parserItem) { + cppGenerator.addParseFile(jParser, parserItem); + } + + @Override + public void onParseEnd(JParser jParser) { + cppGenerator.generate(jParser); + } + + @Override + public void onParserComplete(JParser jParser, ArrayList parserItems) { + super.onParserComplete(jParser, parserItems); + + // For each class that has registered MethodHandle entries, inject the FFMHandles inner class + for(JParserItem parserItem : parserItems) { + if(parserItem.notAllowed) continue; + + ClassOrInterfaceDeclaration classDeclaration = parserItem.getClassDeclaration(); + if(classDeclaration == null) continue; + + String className = classDeclaration.getNameAsString(); + if(!registry.hasEntries(className)) continue; + + injectFFMHandlesClass(parserItem.unit, classDeclaration, className); + } + } + + // ==================== FFM Bridge Method Generation ==================== + + /** + * Register a native method in the MethodHandle registry. + */ + private void registerNativeMethod(MethodDeclaration methodDeclaration) { + TypeDeclaration classOrEnum = (TypeDeclaration) methodDeclaration.getParentNode().get(); + CompilationUnit compilationUnit = classOrEnum.findCompilationUnit().get(); + String packageName = compilationUnit.getPackageDeclaration().get().getNameAsString(); + String className = classOrEnum.getNameAsString(); + String methodName = methodDeclaration.getNameAsString(); + + // Build parameter info + List paramInfos = new ArrayList<>(); + ArrayList ffmArgs = new ArrayList<>(); + if(methodDeclaration.getParameters() != null) { + for(Parameter parameter : methodDeclaration.getParameters()) { + FFMMethodHandleRegistry.ParamInfo paramInfo = FFMMethodHandleRegistry.ParamInfo.fromParameter(parameter); + paramInfos.add(paramInfo); + + String[] typeTokens = parameter.getType().toString().split("\\."); + String type = typeTokens[typeTokens.length - 1]; + ffmArgs.add(new FFMCppGenerator.FFMArgument( + parameter.getNameAsString(), type, + FFMTypeMapper.getCType(type), + FFMTypeMapper.getOverloadSuffix(type))); + } + } + + String returnType = methodDeclaration.getType().toString(); + String symbolName = FFMCppGenerator.buildSymbolName(packageName, className, methodName, ffmArgs); + + registry.register(className, symbolName, methodName, returnType, paramInfos); + } + + /** + * Transform a JNI-style native method declaration into an FFM bridge method. + * Removes the 'native' modifier and adds a body that invokes the MethodHandle. + */ + private void convertToFFMBridgeMethod(MethodDeclaration methodDeclaration) { + // Remove native modifier + methodDeclaration.removeModifier(Modifier.Keyword.NATIVE); + + String methodName = methodDeclaration.getNameAsString(); + Type returnType = methodDeclaration.getType(); + String returnTypeStr = returnType.asString(); + boolean isVoid = returnType.isVoidType(); + + // Build the invokeExact call arguments + StringBuilder invokeArgs = new StringBuilder(); + NodeList parameters = methodDeclaration.getParameters(); + for(int i = 0; i < parameters.size(); i++) { + Parameter parameter = parameters.get(i); + if(i > 0) invokeArgs.append(", "); + + String paramType = parameter.getType().asString(); + // For String parameters, we need to convert to MemorySegment + if(paramType.equals("String")) { + invokeArgs.append("(java.lang.foreign.MemorySegment)(").append(parameter.getNameAsString()) + .append(" != null ? java.lang.foreign.Arena.global().allocateFrom(") + .append(parameter.getNameAsString()).append(") : java.lang.foreign.MemorySegment.NULL)"); + } + else { + invokeArgs.append(parameter.getNameAsString()); + } + } + + // Build method body + StringBuilder bodyCode = new StringBuilder(); + bodyCode.append("{\n"); + bodyCode.append(" try {\n"); + + if(isVoid) { + bodyCode.append(" FFMHandles.").append(methodName) + .append(".invokeExact(").append(invokeArgs).append(");\n"); + } + else { + String castType = FFMTypeMapper.getFFMCast(returnTypeStr); + bodyCode.append(" return (").append(castType).append(") FFMHandles.").append(methodName) + .append(".invokeExact(").append(invokeArgs).append(");\n"); + } + + bodyCode.append(" } catch(Throwable e) {\n"); + bodyCode.append(" throw new RuntimeException(e);\n"); + bodyCode.append(" }\n"); + + if(!isVoid) { + // Unreachable but makes the compiler happy + } + + bodyCode.append("}"); + + BlockStmt body = StaticJavaParser.parseBlock(bodyCode.toString()); + methodDeclaration.setBody(body); + } + + /** + * Inject the FFMHandles inner class into a Java class with all MethodHandle field declarations. + */ + private void injectFFMHandlesClass(CompilationUnit unit, ClassOrInterfaceDeclaration classDeclaration, String className) { + List entries = registry.getEntries(className); + if(entries.isEmpty()) return; + + // Build the inner class source + StringBuilder sb = new StringBuilder(); + sb.append("private static final class FFMHandles {\n"); + sb.append(" private static final java.lang.foreign.SymbolLookup LOOKUP;\n"); + sb.append(" private static final java.lang.foreign.Linker LINKER = java.lang.foreign.Linker.nativeLinker();\n"); + sb.append(" static {\n"); + sb.append(" LOOKUP = java.lang.foreign.SymbolLookup.loaderLookup();\n"); + sb.append(" }\n\n"); + + for(FFMMethodHandleRegistry.FFMEntry entry : entries) { + String descriptor = FFMMethodHandleRegistry.buildFunctionDescriptor(entry); + sb.append(" static final java.lang.invoke.MethodHandle ").append(entry.javaMethodName) + .append(" = LINKER.downcallHandle(\n"); + sb.append(" LOOKUP.find(\"").append(entry.symbolName).append("\").orElseThrow(),\n"); + sb.append(" ").append(descriptor).append(");\n\n"); + } + + sb.append("}"); + + // Parse and add to the class + ClassOrInterfaceDeclaration innerClass = StaticJavaParser.parseBodyDeclaration(sb.toString()) + .asClassOrInterfaceDeclaration(); + classDeclaration.addMember(innerClass); + + // Add FFM imports + unit.addImport("java.lang.foreign.FunctionDescriptor"); + unit.addImport("java.lang.foreign.ValueLayout"); + unit.addImport("java.lang.foreign.Linker"); + unit.addImport("java.lang.foreign.SymbolLookup"); + unit.addImport("java.lang.foreign.Arena"); + unit.addImport("java.lang.foreign.MemorySegment"); + unit.addImport("java.lang.invoke.MethodHandle"); + } + + // ==================== C++ Parameter Helpers (reused from CppCodeParser) ==================== + + private void setupMethodGenerated(IDLMethod idlMethod, String param, + ClassOrInterfaceDeclaration classDeclaration, + MethodDeclaration methodDeclaration, + MethodDeclaration nativeMethod) { + Type returnType = methodDeclaration.getType(); + String returnTypeStr = idlMethod.getJavaReturnType(); + String cppReturnType = idlMethod.getCPPReturnType(); + String methodName = idlMethod.getCPPName(); + String classTypeName = classDeclaration.getNameAsString(); + IDLClass idlClass = idlMethod.idlFile.getClass(classTypeName); + if(idlClass != null) { + classTypeName = idlClass.getCPPName(); + } + String returnCastStr = ""; + String methodCaller = methodName + "(" + param + ")"; + if(idlMethod.idlFile.getEnum(returnTypeStr) != null) { + returnCastStr = "(int)"; + } + if(idlMethod.isAny) { + returnCastStr = "(int64_t)"; + } + + String constTag = ""; + if(idlMethod.isReturnConst) { + constTag = "const "; + } + + String operator = getOperator(idlMethod.operator, param); + String content = null; + IDLMethodOperation.Op op = IDLMethodOperation.getEnum(idlMethod, methodDeclaration, nativeMethod); + switch(op) { + case CALL_VOID_STATIC: + content = METHOD_CALL_VOID_STATIC_TEMPLATE.replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case CALL_VOID: + content = METHOD_CALL_VOID_TEMPLATE.replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_OBJ_REF_POINTER_STATIC: + content = METHOD_GET_REF_OBJ_POINTER_STATIC_TEMPLATE.replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_OBJ_REF_POINTER: + if(operator.isEmpty()) { + content = METHOD_GET_REF_OBJ_POINTER_TEMPLATE.replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName); + } else { + content = METHOD_GET_REF_OBJ_POINTER_OPERATOR_TEMPLATE.replace(TEMPLATE_TAG_OPERATOR, operator).replace(TEMPLATE_TAG_TYPE, classTypeName); + } + break; + case GET_OBJ_VALUE_STATIC: { + String returnTypeName = returnType.asClassOrInterfaceType().asClassOrInterfaceType().getNameAsString(); + IDLClass retTypeClass = idlMethod.idlFile.getClass(returnTypeName); + if(retTypeClass != null) returnTypeName = retTypeClass.getCPPName(); + String copyParam = "copy_addr"; + content = METHOD_GET_OBJ_VALUE_STATIC_TEMPLATE + .replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName) + .replace(TEMPLATE_TAG_COPY_TYPE, returnTypeName).replace(TEMPLATE_TAG_COPY_PARAM, copyParam); + break; + } + case GET_OBJ_VALUE: { + String returnTypeName = returnType.asClassOrInterfaceType().asClassOrInterfaceType().getNameAsString(); + IDLClass retTypeClass = idlMethod.idlFile.getClass(returnTypeName); + if(retTypeClass != null) returnTypeName = retTypeClass.getCPPName(); + String copyParam = "copy_addr"; + if(operator.isEmpty()) { + content = METHOD_GET_OBJ_VALUE_TEMPLATE + .replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName) + .replace(TEMPLATE_TAG_COPY_TYPE, returnTypeName).replace(TEMPLATE_TAG_COPY_PARAM, copyParam); + } else { + content = METHOD_GET_OBJ_VALUE_ARITHMETIC_OPERATOR_TEMPLATE + .replace(TEMPLATE_TAG_OPERATOR, operator).replace(TEMPLATE_TAG_TYPE, classTypeName) + .replace(TEMPLATE_TAG_COPY_TYPE, returnTypeName).replace(TEMPLATE_TAG_COPY_PARAM, copyParam); + } + break; + } + case GET_OBJ_POINTER_STATIC: + content = METHOD_GET_OBJ_POINTER_STATIC_TEMPLATE.replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName); + break; + case GET_OBJ_POINTER: + if(operator.isEmpty()) { + content = METHOD_GET_OBJ_POINTER_TEMPLATE + .replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName) + .replace(TEMPLATE_TAG_RETURN_TYPE, cppReturnType).replace(TEMPLATE_TAG_CONST, constTag); + } else { + content = METHOD_GET_OBJ_POINTER_OPERATOR_TEMPLATE + .replace(TEMPLATE_TAG_OPERATOR, operator).replace(TEMPLATE_TAG_TYPE, classTypeName) + .replace(TEMPLATE_TAG_RETURN_TYPE, cppReturnType).replace(TEMPLATE_TAG_CONST, constTag); + } + break; + case GET_PRIMITIVE_STATIC: + content = METHOD_GET_PRIMITIVE_STATIC_TEMPLATE.replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_CAST, returnCastStr); + break; + case GET_PRIMITIVE: + if(operator.isEmpty()) { + content = METHOD_GET_PRIMITIVE_TEMPLATE.replace(TEMPLATE_TAG_METHOD, methodCaller).replace(TEMPLATE_TAG_TYPE, classTypeName).replace(TEMPLATE_TAG_CAST, returnCastStr); + } else { + content = METHOD_GET_PRIMITIVE_OPERATOR_TEMPLATE + .replace(TEMPLATE_TAG_OPERATOR, operator).replace(TEMPLATE_TAG_TYPE, classTypeName); + } + break; + } + + String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]"; + String blockComment = header + content; + nativeMethod.setBlockComment(blockComment); + } + + private static String getOperator(String operatorCode, String param) { + String oper = ""; + if(!operatorCode.isEmpty()) { + if(operatorCode.equals("[]")) { + oper = "(*nativeObject)[" + param + "]"; + } else { + oper = "(*nativeObject " + operatorCode + " " + param + ")"; + } + } + return oper; + } + + private static String getParams(IDLMethod idlMethod, MethodDeclaration methodDeclaration) { + NodeList parameters = methodDeclaration.getParameters(); + ArrayList idParameters = idlMethod.parameters; + return getParams(parameters, idParameters); + } + + private static String getParams(NodeList parameters, ArrayList idParameters) { + String param = ""; + for(int i = 0; i < parameters.size(); i++) { + Parameter parameter = parameters.get(i); + IDLParameter idlParameter = idParameters.get(i); + Type type = parameter.getType(); + String paramName = getParam(idlParameter, type); + if(i > 0) param += ", "; + param += paramName; + } + return param; + } + + private static String getParam(IDLParameter idlParameter, Type type) { + IDLFile idlFile = idlParameter.idlFile; + String paramName = idlParameter.name; + String cppType = idlParameter.getCPPType(); + String classType = cppType; + boolean isEnum = idlParameter.isEnum(); + boolean isAny = idlParameter.isAny; + boolean isRef = idlParameter.isRef; + boolean isValue = idlParameter.isValue; + boolean isArray = idlParameter.isArray; + boolean isObject = type.isClassOrInterfaceType(); + + if(!isEnum && isObject && !classType.equals("char*")) { + paramName += IDLDefaultCodeParser.NATIVE_PARAM_ADDRESS; + if(isArray) { + String idlType = cppType.replace("[]", "*"); + if(idlParameter.idlClassOrEnum != null && !isRef) { + idlType += "*"; + } + paramName = "(" + idlType + ")" + paramName; + } else { + String idlArrayOrNull = IDLHelper.getIDLArrayClassOrNull(classType); + if(idlArrayOrNull != null) { + classType = idlArrayOrNull; + } + IDLClass paramClass = idlFile.getClass(classType); + if(paramClass != null) { + classType = paramClass.getCPPName(); + } + if(isRef || isValue) { + paramName = "*((" + classType + "* )" + paramName + ")"; + } else if(isAny) { + paramName = "(" + classType + ")" + paramName; + } else { + paramName = "(" + classType + "* )" + paramName; + } + } + } else if(isAny) { + paramName = "( void* )" + paramName; + } else { + if(classType.equals("int")) { + paramName = "(int)" + paramName; + } else if(classType.equals("float")) { + paramName = "(float)" + paramName; + } else if(classType.equals("double")) { + paramName = "(double)" + paramName; + } else if(classType.equals("boolean")) { + paramName = "(bool)" + paramName; + } + } + + IDLEnumClass anEnum = idlFile.getEnum(classType); + if(anEnum != null) { + if(anEnum.typePrefix.equals(classType)) { + paramName = "(" + classType + ")" + paramName; + } else { + paramName = "(" + anEnum.typePrefix + "::" + classType + ")" + paramName; + } + } + return paramName; + } +} + + + diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java new file mode 100644 index 00000000..6c1a760e --- /dev/null +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java @@ -0,0 +1,241 @@ +package com.github.xpenatan.jParser.ffm; + +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.Parameter; +import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.xpenatan.jParser.core.JParser; +import com.github.xpenatan.jParser.core.JParserItem; +import com.github.xpenatan.jParser.core.util.CustomFileDescriptor; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +/** + * Generates FFMGlue.cpp/.h with extern "C" exported functions using standard C types. + * Parallel to NativeCPPGenerator but without any JNI dependencies. + */ +public class FFMCppGenerator implements FFMNativeCodeGenerator { + + public static boolean SKIP_GLUE_CODE = false; + + private String glueCppDestinationDir; + private String cppGlueName = "FFMGlue"; + + StringBuilder mainPrinter = new StringBuilder(); + StringBuilder headerPrinter = new StringBuilder(); + StringBuilder codePrinter = new StringBuilder(); + + private boolean init = true; + + public FFMCppGenerator(String cppDestinationDir) { + try { + this.glueCppDestinationDir = new File(cppDestinationDir, "ffmglue").getCanonicalPath() + File.separator; + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + private void print(PrintType type, String text) { + if(init) { + init = false; + headerPrinter.append("#pragma once\n"); + headerPrinter.append("#include \n"); + headerPrinter.append("\n"); + headerPrinter.append("#ifdef _WIN32\n"); + headerPrinter.append(" #define FFM_EXPORT __declspec(dllexport)\n"); + headerPrinter.append("#else\n"); + headerPrinter.append(" #define FFM_EXPORT __attribute__((visibility(\"default\")))\n"); + headerPrinter.append("#endif\n"); + headerPrinter.append("\n"); + mainPrinter.append("\n"); + mainPrinter.append("extern \"C\" {\n"); + mainPrinter.append("\n"); + } + if(type == PrintType.HEADER) { + headerPrinter.append(text + "\n"); + } + else if(type == PrintType.MAIN) { + mainPrinter.append(text + "\n"); + } + else if(type == PrintType.CODE) { + codePrinter.append(text + "\n"); + } + } + + @Override + public void addNativeCode(Node node, String content) { + Scanner scanner = new Scanner(content); + boolean haveInclude = content.contains("#include"); + while(scanner.hasNextLine()) { + String line = scanner.nextLine().trim(); + if(haveInclude) { + print(PrintType.HEADER, line); + } + else { + print(PrintType.CODE, line); + } + } + scanner.close(); + } + + @Override + public void addNativeCode(MethodDeclaration nativeMethod, String content) { + String methodName = nativeMethod.getNameAsString(); + boolean isStatic = nativeMethod.isStatic(); + TypeDeclaration classOrEnum = (TypeDeclaration) nativeMethod.getParentNode().get(); + CompilationUnit compilationUnit = classOrEnum.findCompilationUnit().get(); + String packageName = compilationUnit.getPackageDeclaration().get().getNameAsString(); + String className = classOrEnum.getNameAsString(); + String packageNameCPP = packageName.replace(".", "_"); + String returnTypeStr = nativeMethod.getType().toString(); + String returnType = FFMTypeMapper.getCType(returnTypeStr); + + // Build parameter list — no JNIEnv*, no jclass/jobject + String params = "("; + ArrayList arguments = new ArrayList<>(); + if(nativeMethod.getParameters() != null) { + for(Parameter parameter : nativeMethod.getParameters()) { + FFMArgument argument = getArgument(parameter); + arguments.add(argument); + } + } + + String paramsType = ""; + String prefixCode = ""; + String suffixCode = ""; + + for(int i = 0; i < arguments.size(); i++) { + FFMArgument argument = arguments.get(i); + String paramName = argument.name; + String cType = argument.cType; + String valueType = argument.overloadSuffix; + paramsType += valueType; + + if(i > 0) { + params += ", "; + } + + // Strings arrive as const char* directly from FFM — no conversion needed + params += cType + " " + paramName; + } + + if(!paramsType.isEmpty()) { + paramsType = "__" + paramsType; + } + else { + paramsType = "__"; + } + + params += ")"; + + // Escape underscores in method/class names for symbol name + String escapedMethodName = methodName.replace("_", "_1"); + String escapedClassName = className.replace("_", "_1"); + + boolean haveReturn = content.lines().anyMatch(s -> s.trim().startsWith("return ")); + if(haveReturn) { + String wrappedLambda = "" + + returnType + " wrappedReturn = [&]() -> " + returnType + " {\n" + + content + + "\n }();"; + + content = wrappedLambda; + suffixCode += "return wrappedReturn;"; + } + + content = prefixCode + "\n" + content + "\n" + suffixCode; + + String fullMethodName = packageNameCPP + "_" + escapedClassName + "_" + escapedMethodName + paramsType + params; + + print(PrintType.MAIN, "FFM_EXPORT " + returnType + " jparser_" + fullMethodName + " {"); + content = "\t" + content.replace("\n", "\n\t"); + print(PrintType.MAIN, content); + print(PrintType.MAIN, "}"); + print(PrintType.MAIN, ""); + } + + /** + * Build the symbol name for a native method. + * Must match exactly with what FFMCodeParser generates for SymbolLookup.find(). + */ + public static String buildSymbolName(String packageName, String className, String methodName, ArrayList arguments) { + String packageNameCPP = packageName.replace(".", "_"); + String escapedClassName = className.replace("_", "_1"); + String escapedMethodName = methodName.replace("_", "_1"); + + String paramsType = ""; + for(FFMArgument argument : arguments) { + paramsType += argument.overloadSuffix; + } + if(!paramsType.isEmpty()) { + paramsType = "__" + paramsType; + } + else { + paramsType = "__"; + } + + return "jparser_" + packageNameCPP + "_" + escapedClassName + "_" + escapedMethodName + paramsType; + } + + @Override + public void addParseFile(JParser jParser, JParserItem parserItem) { + } + + @Override + public void generate(JParser jParser) { + headerPrinter.append("\n"); + + mainPrinter.insert(0, codePrinter); + mainPrinter.insert(0, headerPrinter); + print(PrintType.MAIN, "}"); + String code = mainPrinter.toString(); + + String gluePathStr = glueCppDestinationDir; + String cppGlueHPath = gluePathStr + cppGlueName + ".h"; + String cppGluePath = gluePathStr + cppGlueName + ".cpp"; + CustomFileDescriptor fileDescriptor = new CustomFileDescriptor(cppGlueHPath); + if(!SKIP_GLUE_CODE) { + fileDescriptor.writeString(code, false); + } + + CustomFileDescriptor cppFile = new CustomFileDescriptor(cppGluePath); + String include = "#include \"" + cppGlueName + ".h\""; + cppFile.writeString(include, false); + } + + private FFMArgument getArgument(Parameter parameter) { + String[] typeTokens = parameter.getType().toString().split("\\."); + String type = typeTokens[typeTokens.length - 1]; + String cType = FFMTypeMapper.getCType(type); + String overloadSuffix = FFMTypeMapper.getOverloadSuffix(type); + return new FFMArgument(parameter.getNameAsString(), type, cType, overloadSuffix); + } + + /** + * Represents a function argument with its FFM/C type info. + */ + public static class FFMArgument { + public final String name; + public final String javaType; + public final String cType; + public final String overloadSuffix; + + public FFMArgument(String name, String javaType, String cType, String overloadSuffix) { + this.name = name; + this.javaType = javaType; + this.cType = cType; + this.overloadSuffix = overloadSuffix; + } + } + + enum PrintType { + HEADER, + CODE, + MAIN + } +} + + diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java new file mode 100644 index 00000000..c2059d04 --- /dev/null +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java @@ -0,0 +1,133 @@ +package com.github.xpenatan.jParser.ffm; + +import com.github.javaparser.ast.body.Parameter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Tracks MethodHandle entries per Java class during FFM code generation. + * After all methods are parsed, this registry is used to inject the FFMHandles inner class + * with static MethodHandle fields and FunctionDescriptor initialization. + */ +public class FFMMethodHandleRegistry { + + private final Map> classEntries = new HashMap<>(); + + /** + * Register a native method for a given class. + */ + public void register(String className, String symbolName, String javaMethodName, + String returnType, List parameters) { + List entries = classEntries.computeIfAbsent(className, k -> new ArrayList<>()); + entries.add(new FFMEntry(symbolName, javaMethodName, returnType, parameters)); + } + + /** + * Get all entries for a given class. + */ + public List getEntries(String className) { + return classEntries.getOrDefault(className, new ArrayList<>()); + } + + /** + * Get all class names that have registered entries. + */ + public Iterable getClassNames() { + return classEntries.keySet(); + } + + /** + * Check if a class has any registered entries. + */ + public boolean hasEntries(String className) { + List entries = classEntries.get(className); + return entries != null && !entries.isEmpty(); + } + + /** + * Clear all entries (for reuse). + */ + public void clear() { + classEntries.clear(); + } + + /** + * Generate the FunctionDescriptor code for a single entry. + * Example output: "FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT)" + * Example output: "FunctionDescriptor.ofVoid(ValueLayout.JAVA_LONG)" + */ + public static String buildFunctionDescriptor(FFMEntry entry) { + StringBuilder sb = new StringBuilder(); + boolean isVoid = entry.returnType.equals("void"); + + if(isVoid) { + sb.append("FunctionDescriptor.ofVoid("); + } + else { + String retLayout = FFMTypeMapper.getValueLayout(entry.returnType); + if(retLayout == null) { + // Non-primitive return (object pointer → long) + retLayout = "ValueLayout.JAVA_LONG"; + } + sb.append("FunctionDescriptor.of(").append(retLayout); + if(!entry.parameters.isEmpty()) { + sb.append(", "); + } + } + + for(int i = 0; i < entry.parameters.size(); i++) { + ParamInfo param = entry.parameters.get(i); + String layout = FFMTypeMapper.getValueLayout(param.javaType); + if(layout == null) { + // Non-primitive parameter (object address → long) + layout = "ValueLayout.JAVA_LONG"; + } + if(i > 0) { + sb.append(", "); + } + sb.append(layout); + } + + sb.append(")"); + return sb.toString(); + } + + /** + * Represents a single MethodHandle entry to be generated. + */ + public static class FFMEntry { + public final String symbolName; + public final String javaMethodName; + public final String returnType; + public final List parameters; + + public FFMEntry(String symbolName, String javaMethodName, String returnType, List parameters) { + this.symbolName = symbolName; + this.javaMethodName = javaMethodName; + this.returnType = returnType; + this.parameters = parameters; + } + } + + /** + * Parameter info for building FunctionDescriptor. + */ + public static class ParamInfo { + public final String name; + public final String javaType; + + public ParamInfo(String name, String javaType) { + this.name = name; + this.javaType = javaType; + } + + public static ParamInfo fromParameter(Parameter parameter) { + String[] typeTokens = parameter.getType().toString().split("\\."); + String type = typeTokens[typeTokens.length - 1]; + return new ParamInfo(parameter.getNameAsString(), type); + } + } +} + diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java new file mode 100644 index 00000000..c42f07f7 --- /dev/null +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java @@ -0,0 +1,21 @@ +package com.github.xpenatan.jParser.ffm; + +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.xpenatan.jParser.core.JParser; +import com.github.xpenatan.jParser.core.JParserItem; + +/** + * Interface for generating native C/C++ glue code for FFM. + * Parallel to CppGenerator but decoupled from JNI dependencies. + */ +public interface FFMNativeCodeGenerator { + void addNativeCode(Node node, String content); + + void addNativeCode(MethodDeclaration nativeMethod, String content); + + void addParseFile(JParser jParser, JParserItem parserItem); + + void generate(JParser jParser); +} + diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java new file mode 100644 index 00000000..07fa91c7 --- /dev/null +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java @@ -0,0 +1,107 @@ +package com.github.xpenatan.jParser.ffm; + +import java.util.HashMap; +import java.util.Map; + +/** + * Maps Java types to FFM ValueLayout constants and C types for the FFM code generator. + */ +public class FFMTypeMapper { + + private static final Map javaToValueLayout = new HashMap<>(); + private static final Map javaToCType = new HashMap<>(); + private static final Map javaToFFMCast = new HashMap<>(); + + static { + // Java primitive → ValueLayout constant name + javaToValueLayout.put("long", "ValueLayout.JAVA_LONG"); + javaToValueLayout.put("int", "ValueLayout.JAVA_INT"); + javaToValueLayout.put("float", "ValueLayout.JAVA_FLOAT"); + javaToValueLayout.put("double", "ValueLayout.JAVA_DOUBLE"); + javaToValueLayout.put("boolean", "ValueLayout.JAVA_BOOLEAN"); + javaToValueLayout.put("short", "ValueLayout.JAVA_SHORT"); + javaToValueLayout.put("byte", "ValueLayout.JAVA_BYTE"); + javaToValueLayout.put("char", "ValueLayout.JAVA_CHAR"); + javaToValueLayout.put("String", "ValueLayout.ADDRESS"); + + // Java primitive → C type for FFMGlue.cpp + javaToCType.put("long", "int64_t"); + javaToCType.put("int", "int32_t"); + javaToCType.put("float", "float"); + javaToCType.put("double", "double"); + javaToCType.put("boolean", "int32_t"); + javaToCType.put("short", "int16_t"); + javaToCType.put("byte", "int8_t"); + javaToCType.put("char", "uint16_t"); + javaToCType.put("void", "void"); + javaToCType.put("String", "const char*"); + + // Java primitive → cast needed in invokeExact return + javaToFFMCast.put("long", "long"); + javaToFFMCast.put("int", "int"); + javaToFFMCast.put("float", "float"); + javaToFFMCast.put("double", "double"); + javaToFFMCast.put("boolean", "boolean"); + javaToFFMCast.put("short", "short"); + javaToFFMCast.put("byte", "byte"); + javaToFFMCast.put("char", "char"); + } + + /** + * Returns the FFM ValueLayout constant for a Java type string. + * Returns null if the type is not a known primitive/String. + */ + public static String getValueLayout(String javaType) { + return javaToValueLayout.get(javaType); + } + + /** + * Returns the C type for a Java type string (used in FFMGlue.cpp). + */ + public static String getCType(String javaType) { + String cType = javaToCType.get(javaType); + return cType != null ? cType : "int64_t"; // default: object addresses are int64_t + } + + /** + * Returns the cast type for MethodHandle.invokeExact() return. + */ + public static String getFFMCast(String javaType) { + String cast = javaToFFMCast.get(javaType); + return cast != null ? cast : "long"; // default: object addresses are long + } + + /** + * Returns true if the type is a known primitive type (including void). + */ + public static boolean isPrimitive(String javaType) { + return javaToCType.containsKey(javaType) && !javaType.equals("String"); + } + + /** + * Returns true if the type is String. + */ + public static boolean isString(String javaType) { + return "String".equals(javaType); + } + + /** + * Gets the overload suffix character for a parameter type, similar to JNI mangling. + * Used to disambiguate overloaded native function names. + */ + public static String getOverloadSuffix(String javaType) { + switch(javaType) { + case "boolean": return "Z"; + case "byte": return "B"; + case "char": return "C"; + case "short": return "S"; + case "int": return "I"; + case "long": return "J"; + case "float": return "F"; + case "double": return "D"; + case "String": return "Ljava_lang_String_2"; + default: return "Ljava_lang_Object_2"; + } + } +} + diff --git a/settings.gradle.kts b/settings.gradle.kts index 7fa2326a..ab0a94a8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,7 @@ include(":jParser:jParser-base") include(":jParser:jParser-idl") include(":jParser:jParser-cpp") include(":jParser:jParser-teavm") +include(":jParser:jParser-ffm") include(":idl:idl-core") include(":idl:idl-teavm") From 1ec2777786743b919709584011f8c35df855c900 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 23 Mar 2026 23:01:39 -0300 Subject: [PATCH 03/12] Phase 2 --- LOCAL_AGENT.md | 44 +++++++++---------- .../jParser/builder/tool/BuilderTool.java | 10 +++++ .../jParser/builder/DefaultBuildTarget.java | 12 +++++ .../builder/tool/BuildToolOptions.java | 7 +++ 4 files changed, 51 insertions(+), 22 deletions(-) diff --git a/LOCAL_AGENT.md b/LOCAL_AGENT.md index bf251dfb..9f0a458d 100644 --- a/LOCAL_AGENT.md +++ b/LOCAL_AGENT.md @@ -1,28 +1,28 @@ # LOCAL_AGENT.md — Session State ## Current Task -FFM (Foreign Function & Memory API) — Phase 1 implementation complete. +FFM (Foreign Function & Memory API) — Phase 2: Build Integration — **COMPLETE** ## Current Progress -- [x] Deep codebase analysis: traced full JNI and TeaVM code generation pipelines -- [x] Created `FFM_PLAN.md` with 4-phase implementation plan -- [x] **Task 1.1**: Created `jParser/jParser-ffm` Gradle module (Java 11 build-time, generates Java 24 code) -- [x] **Task 1.1**: Registered in `settings.gradle.kts`, `publish.gradle.kts`, `jParser-build-tool/build.gradle.kts` -- [x] **Task 1.2**: Implemented `FFMCodeParser` extending `IDLDefaultCodeParser` with header `"FFM"` -- [x] **Task 1.3**: Implemented `FFMCppGenerator` — emits `extern "C"` functions with `int64_t`/`int32_t` types -- [x] **Task 1.4**: Implemented `FFMMethodHandleRegistry` — tracks MethodHandle entries per class -- [x] **Task 1.4**: Implemented `FFMTypeMapper` — maps Java types to ValueLayout/C types -- [x] Created `FFMNativeCodeGenerator` interface (decoupled from jParser-cpp CppGenerator) -- [x] Compilation verified: `jParser-ffm` and `jParser-build-tool` both compile successfully -- [x] Existing tests pass (jParser-idl:test) +- [x] Phase 1 complete (all tasks 1.1–1.4) +- [x] **Task 2.1**: Extend `BuildToolOptions` with `generateFFM` flag and `moduleFFMPath` +- [x] **Task 2.2**: Extend `BuilderTool.generateAndBuild()` with FFM generation block +- [x] **Task 2.3**: Add `addFFMGlueCode()` helper in `DefaultBuildTarget` +- [x] Compilation verified: `jParser-build`, `jParser-build-tool`, `jParser-ffm` all BUILD SUCCESSFUL +- [x] Existing tests pass (`jParser-idl:test`) ## Next Task -Phase 2 implementation: -1. Extend `BuildToolOptions` with `generateFFM` flag -2. Extend `BuilderTool.generateAndBuild()` with FFM generation block -3. Add FFM build target helpers +Phase 3 implementation: +1. Create `loader/loader-ffm` module +2. Establish `lib-ffm` module convention +3. Update TestLib example -## Files Created +## Files Modified (Phase 2) +- `jParser/jParser-build/src/main/java/.../tool/BuildToolOptions.java` — Added `generateFFM`, `moduleFFMPath`, `getModuleFFMPath()` +- `jParser/jParser-build-tool/src/main/java/.../tool/BuilderTool.java` — Added FFM generation block with `FFMCodeParser` + `FFMCppGenerator` +- `jParser/jParser-build/src/main/java/.../DefaultBuildTarget.java` — Added `addFFMGlueCode()` method + +## Files Created (Phase 1) - `jParser/jParser-ffm/build.gradle.kts` - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java` - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java` @@ -30,15 +30,15 @@ Phase 2 implementation: - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java` - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java` -## Files Modified +## Files Modified (Phase 1) - `settings.gradle.kts` — Added `:jParser:jParser-ffm` - `buildSrc/src/main/kotlin/publish.gradle.kts` — Added to `libProjects` - `jParser/jParser-build-tool/build.gradle.kts` — Added dependency on `jParser-ffm` ## Key Design Decisions -- `jParser-ffm` module compiles on Java 11 (it's a build-time code generator); the **generated output** targets Java 24 -- Created `FFMNativeCodeGenerator` interface instead of depending on `jParser-cpp`'s `CppGenerator` (decoupled) +- `jParser-ffm` module compiles on Java 11 (build-time code generator); generated output targets Java 24 +- Created `FFMNativeCodeGenerator` interface instead of depending on `jParser-cpp`'s `CppGenerator` - C++ templates use `int64_t`/`int32_t` casts instead of `jlong`/`jint` -- Symbol naming: `jparser_____` +- Symbol naming: `jparser_____` - FFM bridge methods use `MethodHandle.invokeExact()` wrapped in try/catch -- `SymbolLookup.loaderLookup()` used for library resolution (relies on `System.loadLibrary`) +- `SymbolLookup.loaderLookup()` used for library resolution diff --git a/jParser/jParser-build-tool/src/main/java/com/github/xpenatan/jParser/builder/tool/BuilderTool.java b/jParser/jParser-build-tool/src/main/java/com/github/xpenatan/jParser/builder/tool/BuilderTool.java index f008b709..493a4ce6 100644 --- a/jParser/jParser-build-tool/src/main/java/com/github/xpenatan/jParser/builder/tool/BuilderTool.java +++ b/jParser/jParser-build-tool/src/main/java/com/github/xpenatan/jParser/builder/tool/BuilderTool.java @@ -7,6 +7,8 @@ import com.github.xpenatan.jParser.cpp.CppCodeParser; import com.github.xpenatan.jParser.cpp.CppGenerator; import com.github.xpenatan.jParser.cpp.NativeCPPGenerator; +import com.github.xpenatan.jParser.ffm.FFMCodeParser; +import com.github.xpenatan.jParser.ffm.FFMCppGenerator; import com.github.xpenatan.jParser.idl.IDLFile; import com.github.xpenatan.jParser.idl.IDLRenaming; import com.github.xpenatan.jParser.idl.IDLReader; @@ -56,6 +58,14 @@ private static void generateAndBuild(BuildToolOptions op, BuildToolListener list JParser.generate(teavmParser, op.getModuleBaseJavaDir(), op.getModuleTeaVMPath() + "/src/main/java/"); } + if(op.generateFFM) { + FFMCppGenerator ffmGenerator = new FFMCppGenerator(op.getCPPDestinationPath()); + FFMCodeParser ffmParser = new FFMCodeParser(ffmGenerator, idlReader, op.packageName, op.getSourceDir()); + ffmParser.generateClass = true; + ffmParser.idlRenaming = packageRenaming; + JParser.generate(ffmParser, op.getModuleBaseJavaDir(), op.getModuleFFMPath() + "/src/main/java"); + } + BuildConfig buildConfig = new BuildConfig(op); JBuilder.build(buildConfig, targets); } diff --git a/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/DefaultBuildTarget.java b/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/DefaultBuildTarget.java index 184831b5..919afc8e 100644 --- a/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/DefaultBuildTarget.java +++ b/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/DefaultBuildTarget.java @@ -273,6 +273,18 @@ else if(isMac()) { } } + /** + * Add FFM glue code for compilation. Unlike JNI, no JNI headers are needed. + * The FFMGlue.cpp/.h files are generated by FFMCppGenerator into the ffmglue/ subdirectory. + * + * @param libBuildCPPPath the module build C++ path (from BuildToolOptions.getModuleBuildCPPPath()) + */ + public void addFFMGlueCode(String libBuildCPPPath) { + String ffmGlueDir = libBuildCPPPath + "/src/ffmglue"; + headerDirs.add("-I" + ffmGlueDir); + cppInclude.add(ffmGlueDir + "/FFMGlue.cpp"); + } + public static ArrayList getCPPFiles(CustomFileDescriptor dir, ArrayList cppIncludes, ArrayList cppExcludes) { ArrayList files = new ArrayList<>(); getAllFiles(dir, files); diff --git a/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java b/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java index 12e81374..6e95d530 100644 --- a/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java +++ b/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java @@ -14,6 +14,7 @@ public class BuildToolOptions { public final String packageName; public boolean generateTeaVM = true; public boolean generateCPP = true; + public boolean generateFFM = false; /** Name of the idl file located in [Module Build Path] + src/main/cpp/myidl.idl. The default is libName but can be changed. */ public String idlName; @@ -24,6 +25,7 @@ public class BuildToolOptions { private String moduleBuildCPPPath; private String moduleCorePath; private String moduleTeavmPath; + private String moduleFFMPath; private ArrayList idlPath = new ArrayList<>(); private ArrayList idlPathRef = new ArrayList<>(); private ArrayList additionalSourceDirs = new ArrayList<>(); @@ -75,6 +77,7 @@ private void setup() { moduleBuildPath = modulePath + "/" + modulePrefix + "-build"; moduleCorePath = modulePath + "/" + modulePrefix + "-core"; moduleTeavmPath = modulePath + "/" + modulePrefix + "-teavm"; + moduleFFMPath = modulePath + "/" + modulePrefix + "-ffm"; moduleBaseJavaDir = moduleBasePath + "/src/main/java"; cppPath = moduleBuildPath + "/src/main/cpp/"; @@ -137,6 +140,10 @@ public String getModuleTeaVMPath() { return moduleTeavmPath; } + public String getModuleFFMPath() { + return moduleFFMPath; + } + public IDLFile[] getIDL() { IDLFile [] path = new IDLFile[idlPath.size()]; idlPath.toArray(path); From b0064883f6a6cdf80837f4d59fc29f54686dffd0 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 23 Mar 2026 23:08:50 -0300 Subject: [PATCH 04/12] Phase 3 --- LOCAL_AGENT.md | 62 ++++--- .../TestLib/lib/lib-build/build.gradle.kts | 44 ++++- .../lib/lib-build/src/main/java/BuildLib.java | 157 ++++++++++++++++++ examples/TestLib/lib/lib-ffm/build.gradle.kts | 32 ++++ settings.gradle.kts | 1 + 5 files changed, 273 insertions(+), 23 deletions(-) create mode 100644 examples/TestLib/lib/lib-ffm/build.gradle.kts diff --git a/LOCAL_AGENT.md b/LOCAL_AGENT.md index 9f0a458d..83476971 100644 --- a/LOCAL_AGENT.md +++ b/LOCAL_AGENT.md @@ -1,28 +1,41 @@ # LOCAL_AGENT.md — Session State ## Current Task -FFM (Foreign Function & Memory API) — Phase 2: Build Integration — **COMPLETE** +FFM (Foreign Function & Memory API) — Phase 3: Runtime & Downstream Module Convention — **COMPLETE** ## Current Progress - [x] Phase 1 complete (all tasks 1.1–1.4) -- [x] **Task 2.1**: Extend `BuildToolOptions` with `generateFFM` flag and `moduleFFMPath` -- [x] **Task 2.2**: Extend `BuilderTool.generateAndBuild()` with FFM generation block -- [x] **Task 2.3**: Add `addFFMGlueCode()` helper in `DefaultBuildTarget` -- [x] Compilation verified: `jParser-build`, `jParser-build-tool`, `jParser-ffm` all BUILD SUCCESSFUL +- [x] Phase 2 complete (all tasks 2.1–2.3) +- [x] **Task 3.1**: Decision on `loader-ffm` — skipped (SymbolLookup.loaderLookup() works with loader-core) +- [x] **Task 3.2**: Created TestLib `lib-ffm` module (generated FFM Java destination) +- [x] **Task 3.3**: Updated TestLib `BuildLib.java` with FFM arg handling + 4 FFM platform build target methods +- [x] **Task 3.4**: Added 5 FFM Gradle tasks to TestLib `lib-build/build.gradle.kts` +- [x] Compilation verified: `lib-ffm`, `lib-build` both BUILD SUCCESSFUL - [x] Existing tests pass (`jParser-idl:test`) ## Next Task -Phase 3 implementation: -1. Create `loader/loader-ffm` module -2. Establish `lib-ffm` module convention -3. Update TestLib example +Phase 4 (Advanced — deferred per plan): +- Task 4.1: Callback support via `upcallStub` +- Task 4.2: Array/Buffer optimization +- Task 4.3: `[-FFM;-NATIVE]` code block support -## Files Modified (Phase 2) -- `jParser/jParser-build/src/main/java/.../tool/BuildToolOptions.java` — Added `generateFFM`, `moduleFFMPath`, `getModuleFFMPath()` -- `jParser/jParser-build-tool/src/main/java/.../tool/BuilderTool.java` — Added FFM generation block with `FFMCodeParser` + `FFMCppGenerator` -- `jParser/jParser-build/src/main/java/.../DefaultBuildTarget.java` — Added `addFFMGlueCode()` method +## Files Created (Phase 3) +- `examples/TestLib/lib/lib-ffm/build.gradle.kts` — FFM generated output module (mirrors lib-core) + +## Files Modified (Phase 3) +- `settings.gradle.kts` — Added `include(":examples:TestLib:lib:lib-ffm")` +- `examples/TestLib/lib/lib-build/src/main/java/BuildLib.java` — Added FFM arg handling + 4 FFM target methods +- `examples/TestLib/lib/lib-build/build.gradle.kts` — Added 5 FFM Gradle tasks + +## Key Design Decisions (Phase 3) +- **No loader-ffm needed**: `SymbolLookup.loaderLookup()` already works with `loader-core`'s `System.load()` +- **FFM targets output to `/ffm/`**: avoids conflict with JNI `.dll`/`.so`/`.dylib` files +- **Separate arg namespace**: `ffm_windows64` vs `windows64` keeps JNI and FFM builds independent +- **lib-ffm targets Java 11 for now**: Structure is ready; requires Java 24 to compile generated code + +## Full File Change Summary (all phases) -## Files Created (Phase 1) +### Phase 1 — Created - `jParser/jParser-ffm/build.gradle.kts` - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java` - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java` @@ -30,15 +43,20 @@ Phase 3 implementation: - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java` - `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java` -## Files Modified (Phase 1) +### Phase 1 — Modified - `settings.gradle.kts` — Added `:jParser:jParser-ffm` - `buildSrc/src/main/kotlin/publish.gradle.kts` — Added to `libProjects` - `jParser/jParser-build-tool/build.gradle.kts` — Added dependency on `jParser-ffm` -## Key Design Decisions -- `jParser-ffm` module compiles on Java 11 (build-time code generator); generated output targets Java 24 -- Created `FFMNativeCodeGenerator` interface instead of depending on `jParser-cpp`'s `CppGenerator` -- C++ templates use `int64_t`/`int32_t` casts instead of `jlong`/`jint` -- Symbol naming: `jparser_____` -- FFM bridge methods use `MethodHandle.invokeExact()` wrapped in try/catch -- `SymbolLookup.loaderLookup()` used for library resolution +### Phase 2 — Modified +- `jParser/jParser-build/src/main/java/.../tool/BuildToolOptions.java` — Added `generateFFM`, `moduleFFMPath`, `getModuleFFMPath()` +- `jParser/jParser-build-tool/src/main/java/.../tool/BuilderTool.java` — Added FFM generation block +- `jParser/jParser-build/src/main/java/.../DefaultBuildTarget.java` — Added `addFFMGlueCode()` method + +### Phase 3 — Created +- `examples/TestLib/lib/lib-ffm/build.gradle.kts` + +### Phase 3 — Modified +- `settings.gradle.kts` — Added `:examples:TestLib:lib:lib-ffm` +- `examples/TestLib/lib/lib-build/src/main/java/BuildLib.java` — FFM arg handling + FFM targets +- `examples/TestLib/lib/lib-build/build.gradle.kts` — FFM Gradle tasks diff --git a/examples/TestLib/lib/lib-build/build.gradle.kts b/examples/TestLib/lib/lib-build/build.gradle.kts index 205fbbbb..e35a5198 100644 --- a/examples/TestLib/lib/lib-build/build.gradle.kts +++ b/examples/TestLib/lib/lib-build/build.gradle.kts @@ -102,4 +102,46 @@ tasks.register("TestLib_build_project_ios") { mainClass.set(mainClassName) args = mutableListOf("ios") classpath = sourceSets["main"].runtimeClasspath -} \ No newline at end of file +} + +// FFM tasks — generate FFM Java code and/or compile native libs with FFMGlue + +tasks.register("TestLib_build_project_ffm") { + group = "lib" + description = "Generate FFM Java code only (no native compilation)" + mainClass.set(mainClassName) + args = mutableListOf("ffm") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("TestLib_build_project_ffm_windows64") { + group = "lib" + description = "Generate FFM Java code and compile for Windows with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_windows64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("TestLib_build_project_ffm_linux64") { + group = "lib" + description = "Generate FFM Java code and compile for Linux with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_linux64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("TestLib_build_project_ffm_mac64") { + group = "lib" + description = "Generate FFM Java code and compile for Mac with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_mac64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("TestLib_build_project_ffm_macArm") { + group = "lib" + description = "Generate FFM Java code and compile for Mac ARM with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_macArm") + classpath = sourceSets["main"].runtimeClasspath +} diff --git a/examples/TestLib/lib/lib-build/src/main/java/BuildLib.java b/examples/TestLib/lib/lib-build/src/main/java/BuildLib.java index e2664b13..bb97db07 100644 --- a/examples/TestLib/lib/lib-build/src/main/java/BuildLib.java +++ b/examples/TestLib/lib/lib-build/src/main/java/BuildLib.java @@ -36,6 +36,11 @@ public static void main(String[] args) throws Exception { BuildToolOptions op = new BuildToolOptions(data, args); op.addAdditionalIDLRefPath(IDLReader.getIDLHelperFile()); + // Enable FFM code generation if requested + if(op.containsArg("ffm")) { + op.generateFFM = true; + } + BuilderTool.build(op, new BuildToolListener() { @Override public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList targets) { @@ -61,6 +66,21 @@ public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList/ffm/ to avoid conflicts with JNI libs. + + private static BuildMultiTarget getFFMWindowTarget(BuildToolOptions op) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + // Make a static library (same as JNI — shared C++ source) + WindowsTarget compileStaticTarget = new WindowsTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "windows/ffm"; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + WindowsTarget linkTarget = new WindowsTarget(); + linkTarget.libDirSuffix = "windows/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("-Wl,--whole-archive"); + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/windows/ffm/" + op.libName + "64_.a"); + linkTarget.linkerFlags.add("-Wl,--no-whole-archive"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMWindowVCTarget(BuildToolOptions op) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + // Make a static library + WindowsMSVCTarget compileStaticTarget = new WindowsMSVCTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "windows/vc/ffm"; + compileStaticTarget.cppFlags.add("/std:c++11"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + WindowsMSVCTarget linkTarget = new WindowsMSVCTarget(); + linkTarget.libDirSuffix = "windows/vc/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("/std:c++11"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("/WHOLEARCHIVE:" + libBuildCPPPath + "/libs/windows/vc/ffm/" + op.libName + "64_.lib"); + linkTarget.linkerFlags.add("-DLL"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMLinuxTarget(BuildToolOptions op) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + // Make a static library + LinuxTarget compileStaticTarget = new LinuxTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "linux/ffm"; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + LinuxTarget linkTarget = new LinuxTarget(); + linkTarget.libDirSuffix = "linux/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("-Wl,--whole-archive"); + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/linux/ffm/lib" + op.libName + "64_.a"); + linkTarget.linkerFlags.add("-Wl,--no-whole-archive"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMMacTarget(BuildToolOptions op, boolean isArm) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String macSubDir = isArm ? "mac/arm/ffm" : "mac/ffm"; + + // Make a static library + MacTarget compileStaticTarget = new MacTarget(isArm); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = macSubDir; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + MacTarget linkTarget = new MacTarget(isArm); + linkTarget.libDirSuffix = macSubDir; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("-Wl,-force_load"); + if(isArm) { + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/arm/ffm/lib" + op.libName + "64_.a"); + } + else { + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/ffm/lib" + op.libName + "64_.a"); + } + multiTarget.add(linkTarget); + + return multiTarget; + } } \ No newline at end of file diff --git a/examples/TestLib/lib/lib-ffm/build.gradle.kts b/examples/TestLib/lib/lib-ffm/build.gradle.kts new file mode 100644 index 00000000..f9c93be5 --- /dev/null +++ b/examples/TestLib/lib/lib-ffm/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("java") + id("java-library") +} + +// NOTE: Generated FFM Java code requires Java 24+ to compile (uses java.lang.foreign.*). +// Set to Java 11 as a placeholder so Gradle can configure the module even without Java 24 installed. +java { + sourceCompatibility = JavaVersion.toVersion(LibExt.java11Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java11Target) +} + +dependencies { + if(LibExt.exampleUseRepoLibs) { + api("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-core:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-helper-core:-SNAPSHOT") + } + else { + api(project(":loader:loader-core")) + api(project(":idl:idl-core")) + api(project(":idl-helper:idl-helper-core")) + } +} + +tasks.named("clean") { + doFirst { + val srcPath = "$projectDir/src/main/" + project.delete(files(srcPath)) + } +} + diff --git a/settings.gradle.kts b/settings.gradle.kts index ab0a94a8..468ed56b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,6 +23,7 @@ include(":loader:loader-teavm") include(":examples:TestLib:lib:lib-build") include(":examples:TestLib:lib:lib-base") include(":examples:TestLib:lib:lib-core") +include(":examples:TestLib:lib:lib-ffm") include(":examples:TestLib:lib:lib-desktop") include(":examples:TestLib:lib:lib-teavm") include(":examples:TestLib:lib:lib-android") From 714d40074504589c8409cdf2faa3ae4003690c7e Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 23 Mar 2026 23:27:16 -0300 Subject: [PATCH 05/12] Phase 4 --- LOCAL_AGENT.md | 79 ++--- .../xpenatan/jParser/ffm/FFMCodeParser.java | 324 ++++++++++++++++++ .../xpenatan/jParser/ffm/FFMCppGenerator.java | 6 + .../jParser/ffm/FFMNativeCodeGenerator.java | 6 + .../xpenatan/jParser/ffm/FFMTypeMapper.java | 68 ++++ 5 files changed, 430 insertions(+), 53 deletions(-) diff --git a/LOCAL_AGENT.md b/LOCAL_AGENT.md index 83476971..b07af6f3 100644 --- a/LOCAL_AGENT.md +++ b/LOCAL_AGENT.md @@ -1,62 +1,35 @@ # LOCAL_AGENT.md — Session State ## Current Task -FFM (Foreign Function & Memory API) — Phase 3: Runtime & Downstream Module Convention — **COMPLETE** +FFM (Foreign Function & Memory API) — Phase 4: Advanced Features — **COMPLETE** ## Current Progress - [x] Phase 1 complete (all tasks 1.1–1.4) - [x] Phase 2 complete (all tasks 2.1–2.3) -- [x] **Task 3.1**: Decision on `loader-ffm` — skipped (SymbolLookup.loaderLookup() works with loader-core) -- [x] **Task 3.2**: Created TestLib `lib-ffm` module (generated FFM Java destination) -- [x] **Task 3.3**: Updated TestLib `BuildLib.java` with FFM arg handling + 4 FFM platform build target methods -- [x] **Task 3.4**: Added 5 FFM Gradle tasks to TestLib `lib-build/build.gradle.kts` -- [x] Compilation verified: `lib-ffm`, `lib-build` both BUILD SUCCESSFUL +- [x] Phase 3 complete (all tasks 3.1–3.4) +- [x] **Task 4.1**: Callback support via `upcallStub` in `FFMCodeParser` +- [x] **Task 4.2**: Array/Buffer optimization helpers in `FFMTypeMapper` +- [x] **Task 4.3**: `[-FFM;-NATIVE]` code block support — verified working via existing pipeline +- [x] Compilation verified: `jParser-ffm`, `jParser-build-tool` both BUILD SUCCESSFUL - [x] Existing tests pass (`jParser-idl:test`) -## Next Task -Phase 4 (Advanced — deferred per plan): -- Task 4.1: Callback support via `upcallStub` -- Task 4.2: Array/Buffer optimization -- Task 4.3: `[-FFM;-NATIVE]` code block support - -## Files Created (Phase 3) -- `examples/TestLib/lib/lib-ffm/build.gradle.kts` — FFM generated output module (mirrors lib-core) - -## Files Modified (Phase 3) -- `settings.gradle.kts` — Added `include(":examples:TestLib:lib:lib-ffm")` -- `examples/TestLib/lib/lib-build/src/main/java/BuildLib.java` — Added FFM arg handling + 4 FFM target methods -- `examples/TestLib/lib/lib-build/build.gradle.kts` — Added 5 FFM Gradle tasks - -## Key Design Decisions (Phase 3) -- **No loader-ffm needed**: `SymbolLookup.loaderLookup()` already works with `loader-core`'s `System.load()` -- **FFM targets output to `/ffm/`**: avoids conflict with JNI `.dll`/`.so`/`.dylib` files -- **Separate arg namespace**: `ffm_windows64` vs `windows64` keeps JNI and FFM builds independent -- **lib-ffm targets Java 11 for now**: Structure is ready; requires Java 24 to compile generated code - -## Full File Change Summary (all phases) - -### Phase 1 — Created -- `jParser/jParser-ffm/build.gradle.kts` -- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java` -- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java` -- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java` -- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java` -- `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java` - -### Phase 1 — Modified -- `settings.gradle.kts` — Added `:jParser:jParser-ffm` -- `buildSrc/src/main/kotlin/publish.gradle.kts` — Added to `libProjects` -- `jParser/jParser-build-tool/build.gradle.kts` — Added dependency on `jParser-ffm` - -### Phase 2 — Modified -- `jParser/jParser-build/src/main/java/.../tool/BuildToolOptions.java` — Added `generateFFM`, `moduleFFMPath`, `getModuleFFMPath()` -- `jParser/jParser-build-tool/src/main/java/.../tool/BuilderTool.java` — Added FFM generation block -- `jParser/jParser-build/src/main/java/.../DefaultBuildTarget.java` — Added `addFFMGlueCode()` method - -### Phase 3 — Created -- `examples/TestLib/lib/lib-ffm/build.gradle.kts` - -### Phase 3 — Modified -- `settings.gradle.kts` — Added `:examples:TestLib:lib:lib-ffm` -- `examples/TestLib/lib/lib-build/src/main/java/BuildLib.java` — FFM arg handling + FFM targets -- `examples/TestLib/lib/lib-build/build.gradle.kts` — FFM Gradle tasks +## All Phases Complete + +All 4 phases of the FFM implementation plan are now complete: +- **Phase 1**: Core FFM module & code parser (`FFMCodeParser`, `FFMCppGenerator`, `FFMMethodHandleRegistry`, `FFMTypeMapper`) +- **Phase 2**: Build integration (`BuildToolOptions.generateFFM`, `BuilderTool` FFM block, `DefaultBuildTarget.addFFMGlueCode()`) +- **Phase 3**: Runtime & downstream (`TestLib lib-ffm` module, `BuildLib` FFM targets, Gradle tasks) +- **Phase 4**: Advanced features (callback via `upcallStub`, array/buffer helpers, `[-FFM;-NATIVE]` blocks) + +## Files Modified (Phase 4) +- `jParser/jParser-ffm/src/main/java/.../FFMCodeParser.java` — Added `onIDLCallbackGenerated()` + 7 helper methods for C++ callback class generation and upcall stub creation +- `jParser/jParser-ffm/src/main/java/.../FFMCppGenerator.java` — Added `addCallbackClassCode()` method +- `jParser/jParser-ffm/src/main/java/.../FFMNativeCodeGenerator.java` — Added `addCallbackClassCode()` to interface +- `jParser/jParser-ffm/src/main/java/.../FFMTypeMapper.java` — Added array type mappings + `getArraySegmentCode()`, `getBufferSegmentCode()`, `getArrayElementLayout()`, `isArrayType()` helpers + +## Key Design Decisions (Phase 4) +- **Function pointers instead of JNI callbacks**: C++ callback class stores typed function pointers instead of JNIEnv*/jobject/jmethodID. Virtual methods call function pointers directly. +- **upcallStub with Arena.ofAuto()**: GC-managed arena means stubs are freed when the Java callback is garbage-collected. No manual lifecycle management. +- **Static native setupCallback**: FFM uses explicit `this_addr` param (static method), unlike JNI which uses non-static native with implicit env/obj. +- **Array types map to ValueLayout.ADDRESS**: Arrays are passed as MemorySegment pointers, with C types like `int32_t*`, `float*`, etc. +- **[-FFM;-NATIVE] already works**: The existing pipeline (DefaultCodeParser.parseCodeBlock → CMD_NATIVE → setJavaBodyNativeCMD) handles it correctly for native methods with the FFM header. diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java index c47f754a..d84f2832 100644 --- a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java @@ -426,6 +426,80 @@ public void onIDLEnumMethodGenerated(JParser jParser, IDLEnumClass idlEnum, nativeMethodDeclaration.setBlockComment(blockComment); } + @Override + public void onIDLCallbackGenerated(JParser jParser, IDLClass idlClass, + ClassOrInterfaceDeclaration classDeclaration, + MethodDeclaration callbackDeclaration, + ArrayList>> methods) { + IDLClass idlCallbackClass = idlClass.callbackImpl; + + // 1. Build parameter list for native setupCallback: this_addr + one long per callback method (function pointer) + ArrayList parameterArray = new ArrayList<>(); + for(Pair> pair : methods) { + IDLMethod idlMethod = pair.a; + String fpParamName = idlMethod.getCPPName() + "_fp"; + Parameter fpParam = new Parameter(com.github.javaparser.ast.type.PrimitiveType.longType(), fpParamName); + IDLParameterData data = new IDLParameterData(); + data.parameter = fpParam; + parameterArray.add(data); + } + + Type methodReturnType = callbackDeclaration.getType(); + MethodDeclaration nativeMethodDeclaration = IDLMethodParser.generateNativeMethod( + idlReader, callbackDeclaration.getNameAsString(), parameterArray, methodReturnType, false); + + if(!JParserHelper.containsMethod(classDeclaration, nativeMethodDeclaration)) { + // Keep the method static (FFM uses explicit this_addr, no implicit JNI params) + classDeclaration.getMembers().add(nativeMethodDeclaration); + + // 2. Build setupCallback Java body with upcall stub creation + StringBuilder body = new StringBuilder(); + body.append("{\n"); + body.append(" try {\n"); + + for(Pair> pair : methods) { + IDLMethod idlMethod = pair.a; + MethodDeclaration internalMethod = pair.b.a; + String methodName = idlMethod.getCPPName(); + String internalMethodName = internalMethod.getNameAsString(); + + String methodTypeStr = buildMethodTypeStr(internalMethod); + String funcDescriptor = buildCallbackFunctionDescriptor(internalMethod); + + body.append(" java.lang.invoke.MethodHandle mh_").append(methodName) + .append(" = java.lang.invoke.MethodHandles.lookup().findVirtual(this.getClass(), \"") + .append(internalMethodName).append("\", ").append(methodTypeStr).append(").bindTo(this);\n"); + body.append(" java.lang.foreign.MemorySegment stub_").append(methodName) + .append(" = java.lang.foreign.Linker.nativeLinker().upcallStub(mh_").append(methodName) + .append(", ").append(funcDescriptor).append(", java.lang.foreign.Arena.ofAuto());\n"); + } + + // Call native setupCallback with cPointer + stub addresses + body.append(" ").append(nativeMethodDeclaration.getNameAsString()).append("(cPointer"); + for(Pair> pair : methods) { + IDLMethod idlMethod = pair.a; + body.append(", stub_").append(idlMethod.getCPPName()).append(".address()"); + } + body.append(");\n"); + + body.append(" } catch(Throwable e) {\n"); + body.append(" throw new RuntimeException(e);\n"); + body.append(" }\n"); + body.append("}"); + + BlockStmt blockStmt = StaticJavaParser.parseBlock(body.toString()); + callbackDeclaration.setBody(blockStmt); + + // 3. Set C++ code for the native setupCallback method + String cppSetupBody = generateFFMSetupCallbackCPPBody(idlCallbackClass, methods); + String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]"; + nativeMethodDeclaration.setBlockComment(header + cppSetupBody); + + // 4. Generate C++ callback class and emit it via the generator + generateFFMCPPClass(idlClass, classDeclaration, callbackDeclaration, methods); + } + } + // ==================== Code Block Parsing ==================== @Override @@ -625,6 +699,256 @@ private void injectFFMHandlesClass(CompilationUnit unit, ClassOrInterfaceDeclara unit.addImport("java.lang.invoke.MethodHandle"); } + // ==================== FFM Callback C++ Generation ==================== + + /** + * Generate the full C++ callback class with function pointers and emit it. + * Attaches the class definition as a block comment on the constructor (same pattern as CppCodeParser). + */ + private void generateFFMCPPClass(IDLClass idlClass, ClassOrInterfaceDeclaration classDeclaration, + MethodDeclaration callbackDeclaration, + ArrayList>> methods) { + IDLClass callback = idlClass.callbackImpl; + StringBuilder cppClass = new StringBuilder(); + + // Generate function pointer typedefs + for(Pair> pair : methods) { + IDLMethod idlMethod = pair.a; + MethodDeclaration internalMethod = pair.b.a; + cppClass.append(buildFPTypedef(callback.name, idlMethod, internalMethod)).append("\n"); + } + cppClass.append("\n"); + + // Class definition + cppClass.append("class ").append(callback.getCPPName()).append(" : public ").append(idlClass.getCPPName()).append(" {\n"); + cppClass.append("private:\n"); + + // Function pointer fields + for(Pair> pair : methods) { + IDLMethod idlMethod = pair.a; + MethodDeclaration internalMethod = pair.b.a; + String fpTypeName = buildFPTypeName(callback.name, idlMethod, internalMethod); + cppClass.append("\t").append(fpTypeName).append(" ").append(idlMethod.getCPPName()).append("_ptr;\n"); + } + + cppClass.append("public:\n"); + + // setupCallback method — receives function pointers + cppClass.append("\tvoid ").append(callbackDeclaration.getNameAsString()).append("("); + for(int i = 0; i < methods.size(); i++) { + Pair> pair = methods.get(i); + IDLMethod idlMethod = pair.a; + MethodDeclaration internalMethod = pair.b.a; + String fpTypeName = buildFPTypeName(callback.name, idlMethod, internalMethod); + if(i > 0) cppClass.append(", "); + cppClass.append(fpTypeName).append(" ").append(idlMethod.getCPPName()); + } + cppClass.append(") {\n"); + for(Pair> pair : methods) { + IDLMethod idlMethod = pair.a; + cppClass.append("\t\tthis->").append(idlMethod.getCPPName()).append("_ptr = ").append(idlMethod.getCPPName()).append(";\n"); + } + cppClass.append("\t}\n"); + + // Virtual methods — call function pointers + cppClass.append(generateFFMMethodCallers(idlClass, methods)); + + cppClass.append("};\n"); + + // Emit the C++ class via the generator (before extern "C") + cppGenerator.addCallbackClassCode(cppClass.toString()); + + // Also attach to constructor block comment (same pattern as CppCodeParser) + String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]\n"; + String code = header + cppClass.toString(); + classDeclaration.getConstructors().get(0).setBlockComment(code); + } + + /** + * Generate the C++ body for the native setupCallback method. + * Example: nativeObject->setupCallback((fp_type)fp1, (fp_type)fp2); + */ + private String generateFFMSetupCallbackCPPBody(IDLClass idlCallbackClass, + ArrayList>> methods) { + StringBuilder sb = new StringBuilder(); + sb.append("\n").append(idlCallbackClass.name).append("* nativeObject = (").append(idlCallbackClass.name).append("*)this_addr;\n"); + sb.append("nativeObject->setupCallback("); + for(int i = 0; i < methods.size(); i++) { + Pair> pair = methods.get(i); + IDLMethod idlMethod = pair.a; + MethodDeclaration internalMethod = pair.b.a; + String fpTypeName = buildFPTypeName(idlCallbackClass.name, idlMethod, internalMethod); + if(i > 0) sb.append(", "); + sb.append("(").append(fpTypeName).append(")").append(idlMethod.getCPPName()).append("_fp"); + } + sb.append(");\n"); + return sb.toString(); + } + + /** + * Generate virtual method implementations that call function pointers. + */ + private String generateFFMMethodCallers(IDLClass idlClass, + ArrayList>> methods) { + IDLClass callback = idlClass.callbackImpl; + StringBuilder cppMethods = new StringBuilder(); + + for(Pair> pair : methods) { + IDLMethod idlMethod = pair.a; + MethodDeclaration publicMethod = pair.b.b; + + Type type = publicMethod.getType(); + boolean isVoidType = type.isVoidType(); + String returnTypeStr = getFFMCPPType(idlMethod.getCPPReturnType()); + String constStr = idlMethod.isReturnConst ? " const" : ""; + String methodName = idlMethod.getCPPName(); + + // Build virtual method params and call params + StringBuilder methodParams = new StringBuilder(); + StringBuilder callParams = new StringBuilder(); + NodeList publicMethodParameters = publicMethod.getParameters(); + + for(int i = 0; i < idlMethod.parameters.size(); i++) { + IDLParameter idlParameter = idlMethod.parameters.get(i); + Parameter parameter = publicMethodParameters.get(i); + boolean isPrimitive = parameter.getType().isPrimitiveType() || idlParameter.isAny; + String paramName = idlParameter.name; + String paramType = idlParameter.getCPPType(); + boolean isString = idlParameter.idlType.equals("DOMString"); + String tag = " "; + String callParamCast = ""; + + if(!isString) { + if(idlParameter.isRef) { + tag = "& "; + callParamCast = "(int64_t)&"; + } + else if(!idlParameter.isEnum() && !isPrimitive && !idlParameter.isValue) { + tag = "* "; + callParamCast = "(int64_t)"; + } + } + + paramType = getFFMCPPType(paramType); + if(idlParameter.isConst) { + paramType = "const " + paramType; + } + + if(i > 0) { + callParams.append(", "); + methodParams.append(", "); + } + callParams.append(callParamCast).append(paramName); + methodParams.append(paramType).append(tag).append(paramName); + } + + String returnStr = isVoidType ? "" : "return (" + returnTypeStr + ")"; + if(returnTypeStr.contains("unsigned")) { + returnStr = "return (" + returnTypeStr + ")"; + } + + cppMethods.append("\tvirtual ").append(returnTypeStr).append(" ").append(methodName) + .append("(").append(methodParams).append(")").append(constStr).append(" {\n"); + cppMethods.append("\t\t").append(returnStr).append(methodName).append("_ptr(").append(callParams).append(");\n"); + cppMethods.append("\t}\n"); + } + return cppMethods.toString(); + } + + /** + * Build a function pointer typedef for a callback method. + * Example: typedef void (*fp_MyCallbackImpl_onEvent_JJ)(int64_t, int64_t); + */ + private String buildFPTypedef(String className, IDLMethod idlMethod, MethodDeclaration internalMethod) { + String fpTypeName = buildFPTypeName(className, idlMethod, internalMethod); + String returnCType = FFMTypeMapper.getCType(internalMethod.getType().asString()); + + StringBuilder sb = new StringBuilder(); + sb.append("typedef ").append(returnCType).append(" (*").append(fpTypeName).append(")("); + NodeList params = internalMethod.getParameters(); + for(int i = 0; i < params.size(); i++) { + if(i > 0) sb.append(", "); + String paramType = params.get(i).getType().asString(); + sb.append(FFMTypeMapper.getCType(paramType)); + } + sb.append(");"); + return sb.toString(); + } + + /** + * Build a unique function pointer type name for a callback method. + * Example: fp_MyCallbackImpl_onEvent_JJ + */ + private String buildFPTypeName(String className, IDLMethod idlMethod, MethodDeclaration internalMethod) { + StringBuilder suffix = new StringBuilder(); + NodeList params = internalMethod.getParameters(); + for(Parameter param : params) { + suffix.append(FFMTypeMapper.getOverloadSuffix(param.getType().asString())); + } + return "fp_" + className + "_" + idlMethod.getCPPName() + "_" + suffix; + } + + /** + * Build MethodType string for MethodHandles.lookup().findVirtual(). + * Example: java.lang.invoke.MethodType.methodType(void.class, long.class, long.class) + */ + private String buildMethodTypeStr(MethodDeclaration internalMethod) { + StringBuilder sb = new StringBuilder(); + sb.append("java.lang.invoke.MethodType.methodType("); + Type returnType = internalMethod.getType(); + if(returnType.isVoidType()) { + sb.append("void.class"); + } else { + sb.append(returnType.asString()).append(".class"); + } + for(Parameter param : internalMethod.getParameters()) { + sb.append(", ").append(param.getType().asString()).append(".class"); + } + sb.append(")"); + return sb.toString(); + } + + /** + * Build FunctionDescriptor for upcall stubs. + * Example: java.lang.foreign.FunctionDescriptor.ofVoid(java.lang.foreign.ValueLayout.JAVA_LONG) + */ + private String buildCallbackFunctionDescriptor(MethodDeclaration internalMethod) { + StringBuilder sb = new StringBuilder(); + Type returnType = internalMethod.getType(); + boolean isVoid = returnType.isVoidType(); + + if(isVoid) { + sb.append("java.lang.foreign.FunctionDescriptor.ofVoid("); + } else { + String retLayout = FFMTypeMapper.getValueLayout(returnType.asString()); + if(retLayout == null) retLayout = "java.lang.foreign.ValueLayout.JAVA_LONG"; + else retLayout = "java.lang.foreign." + retLayout; + sb.append("java.lang.foreign.FunctionDescriptor.of(").append(retLayout); + if(internalMethod.getParameters().size() > 0) sb.append(", "); + } + + NodeList params = internalMethod.getParameters(); + for(int i = 0; i < params.size(); i++) { + if(i > 0) sb.append(", "); + String paramType = params.get(i).getType().asString(); + String layout = FFMTypeMapper.getValueLayout(paramType); + if(layout == null) layout = "java.lang.foreign.ValueLayout.JAVA_LONG"; + else layout = "java.lang.foreign." + layout; + sb.append(layout); + } + sb.append(")"); + return sb.toString(); + } + + /** + * Map Java/IDL type to FFM-compatible C++ type. + */ + private String getFFMCPPType(String typeString) { + if(typeString.equals("boolean")) return "bool"; + if(typeString.equals("String")) return "char*"; + return typeString; + } + // ==================== C++ Parameter Helpers (reused from CppCodeParser) ==================== private void setupMethodGenerated(IDLMethod idlMethod, String param, diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java index 6c1a760e..67fe2ac0 100644 --- a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java @@ -81,6 +81,12 @@ public void addNativeCode(Node node, String content) { scanner.close(); } + @Override + public void addCallbackClassCode(String cppClassCode) { + // Callback class code goes into the CODE section (before extern "C") + print(PrintType.CODE, cppClassCode); + } + @Override public void addNativeCode(MethodDeclaration nativeMethod, String content) { String methodName = nativeMethod.getNameAsString(); diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java index c42f07f7..11b631f8 100644 --- a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMNativeCodeGenerator.java @@ -14,6 +14,12 @@ public interface FFMNativeCodeGenerator { void addNativeCode(MethodDeclaration nativeMethod, String content); + /** + * Add raw C++ code for a callback class definition. + * This code is placed before the extern "C" block in the generated glue file. + */ + void addCallbackClassCode(String cppClassCode); + void addParseFile(JParser jParser, JParserItem parserItem); void generate(JParser jParser); diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java index 07fa91c7..eff86110 100644 --- a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java @@ -24,6 +24,16 @@ public class FFMTypeMapper { javaToValueLayout.put("char", "ValueLayout.JAVA_CHAR"); javaToValueLayout.put("String", "ValueLayout.ADDRESS"); + // Array types → ADDRESS layout (passed as MemorySegment pointers) + javaToValueLayout.put("int[]", "ValueLayout.ADDRESS"); + javaToValueLayout.put("long[]", "ValueLayout.ADDRESS"); + javaToValueLayout.put("float[]", "ValueLayout.ADDRESS"); + javaToValueLayout.put("double[]", "ValueLayout.ADDRESS"); + javaToValueLayout.put("byte[]", "ValueLayout.ADDRESS"); + javaToValueLayout.put("short[]", "ValueLayout.ADDRESS"); + javaToValueLayout.put("boolean[]", "ValueLayout.ADDRESS"); + javaToValueLayout.put("char[]", "ValueLayout.ADDRESS"); + // Java primitive → C type for FFMGlue.cpp javaToCType.put("long", "int64_t"); javaToCType.put("int", "int32_t"); @@ -36,6 +46,16 @@ public class FFMTypeMapper { javaToCType.put("void", "void"); javaToCType.put("String", "const char*"); + // Array types → C pointer types + javaToCType.put("int[]", "int32_t*"); + javaToCType.put("long[]", "int64_t*"); + javaToCType.put("float[]", "float*"); + javaToCType.put("double[]", "double*"); + javaToCType.put("byte[]", "int8_t*"); + javaToCType.put("short[]", "int16_t*"); + javaToCType.put("boolean[]", "int32_t*"); + javaToCType.put("char[]", "uint16_t*"); + // Java primitive → cast needed in invokeExact return javaToFFMCast.put("long", "long"); javaToFFMCast.put("int", "int"); @@ -103,5 +123,53 @@ public static String getOverloadSuffix(String javaType) { default: return "Ljava_lang_Object_2"; } } + + // ==================== Array/Buffer Optimization Helpers ==================== + + /** + * Returns true if the type is a Java array type. + */ + public static boolean isArrayType(String javaType) { + return javaType.endsWith("[]"); + } + + /** + * Returns FFM code to create a MemorySegment from a Java primitive array. + * Example: "java.lang.foreign.MemorySegment.ofArray(myArray)" + * + * @param paramName the Java variable name of the array + * @param javaType the Java array type (e.g., "int[]", "float[]") + * @return the FFM MemorySegment creation code + */ + public static String getArraySegmentCode(String paramName, String javaType) { + if(!isArrayType(javaType)) { + throw new IllegalArgumentException("Not an array type: " + javaType); + } + return "java.lang.foreign.MemorySegment.ofArray(" + paramName + ")"; + } + + /** + * Returns FFM code to create a MemorySegment from a direct ByteBuffer. + * Example: "java.lang.foreign.MemorySegment.ofBuffer(myBuffer)" + * + * @param paramName the Java variable name of the ByteBuffer + * @return the FFM MemorySegment creation code + */ + public static String getBufferSegmentCode(String paramName) { + return "java.lang.foreign.MemorySegment.ofBuffer(" + paramName + ")"; + } + + /** + * Returns the element ValueLayout for an array type. + * Example: "int[]" → "ValueLayout.JAVA_INT" + * + * @param arrayType the Java array type + * @return the element ValueLayout, or null if not a known array type + */ + public static String getArrayElementLayout(String arrayType) { + if(!isArrayType(arrayType)) return null; + String elementType = arrayType.replace("[]", ""); + return javaToValueLayout.get(elementType); + } } From 751a318eab2524e6d0e58ad118ed3fc8282077a5 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 24 Mar 2026 19:52:00 -0300 Subject: [PATCH 06/12] Fix FFM Issues --- .gitignore | 8 +- AGENTS.md | 149 +++++- FFM_PLAN.md | 473 ------------------ LOCAL_AGENT.md | 35 -- README.md | 2 +- buildSrc/src/main/kotlin/LibExt.kt | 1 + buildSrc/src/main/kotlin/publish.gradle.kts | 1 + examples/SharedLib/app/core/build.gradle.kts | 9 +- .../SharedLib/app/desktop/build.gradle.kts | 13 + .../src/main/java/libA/LibALoader.java | 4 + .../SharedLib/libA/lib-build/build.gradle.kts | 45 +- .../lib-build/src/main/java/BuildLibA.java | 138 +++++ .../libA/lib-desktop/build.gradle.kts | 3 +- .../SharedLib/libA/lib-ffm/build.gradle.kts | 45 ++ .../src/main/java/libB/LibBLoader.java | 4 + .../SharedLib/libB/lib-build/build.gradle.kts | 45 +- .../lib-build/src/main/java/BuildLibB.java | 161 ++++++ .../libB/lib-desktop/build.gradle.kts | 3 +- .../SharedLib/libB/lib-ffm/build.gradle.kts | 34 ++ examples/TestLib/app/android/build.gradle.kts | 1 + examples/TestLib/app/core/build.gradle.kts | 8 +- .../example/app/NativeBridgeBenchmark.java | 363 ++++++++++++++ .../example/app/NativeBridgeBenchmarkApp.java | 34 ++ .../app/NativeBridgeBenchmarkCompare.java | 191 +++++++ .../example/app/NativeBridgeFpsBenchmark.java | 395 +++++++++++++++ .../app/NativeBridgeFpsBenchmarkApp.java | 51 ++ .../app/NativeBridgeFpsBenchmarkCompare.java | 184 +++++++ .../xpenatan/jParser/example/app/TestLib.java | 8 +- examples/TestLib/app/desktop/build.gradle.kts | 173 ++++++- .../app/NativeBridgeBenchmarkMain.java | 14 + .../app/NativeBridgeFpsBenchmarkMain.java | 16 + .../example/testlib/CallbackClassManual.java | 112 +++++ .../testlib/TestBufferManualClass.java | 30 ++ .../example/testlib/TestLibLoader.java | 4 + .../src/main/cpp/source/TestLib/src/TestLib.h | 12 - .../TestLib/lib/lib-desktop/build.gradle.kts | 2 +- examples/TestLib/lib/lib-ffm/build.gradle.kts | 25 +- .../src/main/java/idl/IDLLoader.java | 4 + idl-helper/idl-helper-build/build.gradle.kts | 44 +- .../src/main/java/BuildIDLHelper.java | 110 ++++ .../idl-helper-desktop/build.gradle.kts | 2 +- idl-helper/idl-helper-ffm/build.gradle.kts | 49 ++ .../src/main/java/idl/helper/IDLArray.java | 4 + .../src/main/java/idl/helper/IDLString.java | 8 + .../src/main/java/idl/helper/IDLUtils.java | 30 ++ .../xpenatan/jParser/ffm/FFMCodeParser.java | 150 ++++-- .../jParser/ffm/FFMMethodHandleRegistry.java | 9 +- .../xpenatan/jParser/ffm/FFMTypeMapper.java | 1 + settings.gradle.kts | 3 + 49 files changed, 2613 insertions(+), 597 deletions(-) delete mode 100644 FFM_PLAN.md delete mode 100644 LOCAL_AGENT.md create mode 100644 examples/SharedLib/libA/lib-ffm/build.gradle.kts create mode 100644 examples/SharedLib/libB/lib-ffm/build.gradle.kts create mode 100644 examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmark.java create mode 100644 examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkApp.java create mode 100644 examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkCompare.java create mode 100644 examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmark.java create mode 100644 examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkApp.java create mode 100644 examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkCompare.java create mode 100644 examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java create mode 100644 examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java create mode 100644 idl-helper/idl-helper-ffm/build.gradle.kts diff --git a/.gitignore b/.gitignore index fdc75723..961476b5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,16 +31,22 @@ out/ **/idl-helper/idl-helper-teavm/src/main/java/** **/idl-helper/idl-helper-core/src/main/java/** +**/idl-helper/idl-helper-ffm/src/main/java/** **/webapp/** **/lib/core/src/** **/lib/desktop/src/main/** **/lib/lib-teavm/src/main/java/** **/lib/lib-core/src/main/java/** +**/lib/lib-ffm/src/main/java/** **/libA/lib-teavm/src/main/java/** **/libA/lib-core/src/main/java/** +**/libA/lib-ffm/src/main/java/** **/libB/lib-teavm/src/main/java/** **/libB/lib-core/src/main/java/** +**/libB/lib-ffm/src/main/java/** **/app/android/libs/ .codiumai -.kotlin/ \ No newline at end of file +.kotlin/ + +LOCAL_AGENT.MD \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 509fdaef..f914ffca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,45 +68,148 @@ private static native void internal_native_doSomething(long this_addr); ``` Commands: `-ADD`, `-ADD_RAW`, `-REMOVE`, `-REPLACE`, `-REPLACE_BLOCK`, `-NATIVE`. Use `-IDL_SKIP` on a class comment to prevent IDL generation for that class. +## Requirements + +- **JDK 11+** for building the jParser tool modules +- **JDK 22+** (25 recommended) for FFM modules and running FFM-based apps +- **Gradle** — wrapper included (`./gradlew` / `gradlew.bat`) +- **Windows native builds**: MinGW64 **and/or** Visual Studio C++ (`vcvarsall.bat` must be on PATH) +- **Linux native builds**: GCC / G++ toolchain +- **macOS native builds**: Xcode command-line tools +- **Web builds**: Emscripten SDK (`EMSDK` env var) + ## Build & Run +All commands are run from the **project root**. Use `./gradlew` on Linux/macOS, `gradlew.bat` on Windows. + +### 1. Build the IDL Helper library (required before examples) + +The idl-helper provides the `IDLBase` runtime for all native-bound objects. It must be code-generated and native-compiled before examples can run. + ```sh -# Build everything (from project root) -./gradlew build +# Step 1 — Generate JNI/TeaVM/FFM Java code + compile native library for your platform +# JNI (pick your platform): +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_windows64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_linux64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_mac64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_macArm + +# FFM (pick your platform): +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_ffm_windows64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_ffm_linux64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_ffm_mac64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_ffm_macArm + +# All JNI platforms at once: +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_all +``` -# Generate + compile a specific library (example: TestLib for Windows) +### 2. Build example: TestLib + +TestLib is the primary test/example library. Building it generates Java source code into `lib-core`, `lib-teavm`, and `lib-ffm`, then compiles native C/C++ into platform DLLs/shared-libs. + +```sh +# Step 1 — Generate Java code + compile native for your platform (JNI): ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_windows64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_linux64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_mac64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_macArm -# Build all platforms for TestLib +# All JNI platforms: ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_all -# Platform-specific targets: teavm, windows64, linux64, mac64, macArm, android, ios -# These are passed as args to BuildLib.main() +# FFM (generates lib-ffm Java code + compiles FFM native): +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_windows64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_linux64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_mac64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_macArm + +# Generate FFM Java code only (no native compilation): +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm + +# Step 2 — Run the desktop app: +./gradlew :examples:TestLib:app:desktop:TestLib_run_app_desktop ``` -### Requirements -- **JDK 11+**, **Gradle** (wrapper included) -- **Windows builds**: MinGW64 or Visual Studio C++ (`vcvarsall.bat` must be on PATH) -- **Web builds**: Emscripten SDK (`EMSDK` env var) +### 3. Build example: SharedLib (multi-library) -## Conventions +SharedLib demonstrates two libraries (libA + libB) where libB depends on libA. **Build libA first**, then libB. -- **Version management**: `gradle.properties` holds the version; `LibExt.kt` in `buildSrc/` resolves it. Snapshots use `"-SNAPSHOT"`, releases use the property value. -- **Publishing**: The `publish.gradle.kts` plugin configures all library modules listed in `libProjects`. Use `publishRelease` or `publishTestRelease` tasks. -- **Generated code is not hand-edited**: `lib-core/` and `lib-teavm/` directories contain generated output with a "Do not make changes" header. -- **IDL files** live at `lib-build/src/main/cpp/.idl`. Custom C++ glue code goes in `lib-build/src/main/cpp/custom/`. -- **IDLBase** is the parent of all native-bound classes. Memory must be manually managed via `dispose()`. Use `ClassName.NULL` instead of Java `null` for native parameters. -- **Dependencies**: JavaParser (`3.26.1`) for AST manipulation, TeaVM (`0.13.0`) for web target, JUnit 4 for tests. +```sh +# libA — JNI: +./gradlew :examples:SharedLib:libA:lib-build:LibA_build_project_windows64 +# libA — FFM: +./gradlew :examples:SharedLib:libA:lib-build:LibA_build_project_ffm_windows64 + +# libB — JNI (after libA): +./gradlew :examples:SharedLib:libB:lib-build:LibB_build_project_windows64 +# libB — FFM (after libA): +./gradlew :examples:SharedLib:libB:lib-build:LibB_build_project_ffm_windows64 + +# Run SharedLib desktop app: +./gradlew :examples:SharedLib:app:desktop:SharedLib_run_app_desktop +``` + +Replace `windows64` with `linux64`, `mac64`, or `macArm` for other platforms. + +### 4. JNI vs FFM Benchmarks + +Run micro-benchmarks comparing JNI and FFM bridge overhead. **Requires both JNI and FFM native DLLs to be built first** (see steps 2–3 above). -## Testing +```sh +# Run both benchmarks and print a comparison table +./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_compare + +# Run only JNI benchmark (saves CSV to build/benchmark/benchmark_jni.csv) +./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_jni + +# Run only FFM benchmark (saves CSV to build/benchmark/benchmark_ffm.csv) +./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_ffm +``` + +### 5. JNI vs FFM FPS Benchmarks + +Measures how native bridge overhead affects frame rate. Each frame executes a fixed number of native calls, then returns to let GDX render. Reports average and minimum FPS per scenario. ```sh -# Run IDL parser tests -./gradlew :jParser:jParser-idl:test +# Run both FPS benchmarks and print a comparison table +./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_compare -# Run C++ code parser tests -./gradlew :jParser:jParser-cpp:test +# Run only JNI FPS benchmark (saves CSV to build/benchmark/fps_benchmark_jni.csv) +./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_jni + +# Run only FFM FPS benchmark (saves CSV to build/benchmark/fps_benchmark_ffm.csv) +./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_ffm ``` -Test classes live in standard `src/test/java` directories (e.g., `IDLReaderTest`, `CppCodeParserTest`). +### Build order summary (from scratch on Windows) + +```sh +# 1. Build idl-helper native (both JNI + FFM) +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_windows64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_ffm_windows64 + +# 2. Build TestLib native (both JNI + FFM) +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_windows64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_windows64 + +# 3. Run the desktop app +./gradlew :examples:TestLib:app:desktop:TestLib_run_app_desktop + +# 4. Run throughput benchmarks +./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_compare + +# 5. Run FPS benchmarks +./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_compare +``` + +## Conventions + +- **Version management**: `gradle.properties` holds the version; `LibExt.kt` in `buildSrc/` resolves it. Snapshots use `"-SNAPSHOT"`, releases use the property value. +- **Publishing**: The `publish.gradle.kts` plugin configures all library modules listed in `libProjects`. Use `publishRelease` or `publishTestRelease` tasks. +- **Generated code is not hand-edited**: `lib-core/`, `lib-teavm/`, and `lib-ffm/` directories contain generated output with a "Do not make changes" header. +- **IDL files** live at `lib-build/src/main/cpp/.idl`. Custom C++ glue code goes in `lib-build/src/main/cpp/custom/`. +- **IDLBase** is the parent of all native-bound classes. Memory must be manually managed via `dispose()`. Use `ClassName.NULL` instead of Java `null` for native parameters. +- **Dependencies**: JavaParser (`3.26.1`) for AST manipulation, TeaVM (`0.13.0`) for web target, JUnit 4 for tests. +- **Native bridge selection**: Desktop modules choose `lib-core` (JNI) or `lib-ffm` (FFM) via the `implementation` dependency in `app/desktop/build.gradle.kts`. Only one should be active at a time. diff --git a/FFM_PLAN.md b/FFM_PLAN.md deleted file mode 100644 index 6b2c5c36..00000000 --- a/FFM_PLAN.md +++ /dev/null @@ -1,473 +0,0 @@ -# FFM_PLAN.md — Foreign Function & Memory API Support for jParser - -## Goal - -Add a third code-generation target (alongside JNI and TeaVM) that produces Java classes using the **Foreign Function & Memory (FFM) API** (`java.lang.foreign.*`). FFM eliminates JNI overhead by calling native functions directly through `MethodHandle` downcalls, avoiding JNI parameter marshalling, `JNIEnv*` context, and the `native` keyword entirely. - -The public Java interface (the calling methods) remains **identical** to the JNI path — only the underlying native bridge changes. - ---- - -## Architecture Overview - -### Current Pipeline - -``` -IDL files ──► IDLReader ──► IDLDefaultCodeParser (base class) - │ - ├── CppCodeParser (header "JNI") - │ ├─ Generates: private static native methods - │ ├─ Block comments: [-JNI;-NATIVE] with C++ templates - │ └─ NativeCPPGenerator → JNIGlue.cpp (JNIEXPORT functions) - │ - └── TeaVMCodeParser (header "TEAVM") - ├─ Generates: @JSBody-annotated native methods - └─ Renames to gen.* package -``` - -### Proposed Pipeline (addition) - -``` - ├── FFMCodeParser (header "FFM") - │ ├─ Generates: static MethodHandle fields + private static bridge methods - │ ├─ Block comments: [-FFM;-NATIVE] with C++ templates (no JNI types) - │ └─ FFMCppGenerator → FFMGlue.cpp (extern "C" functions) -``` - -### Why the Same Public Interface Works - -The public methods generated by `IDLMethodParser` / `IDLAttributeParser` / `IDLConstructorParser` call: -```java -internal_native_doSomething(native_address, param1, param2_addr); -``` -These delegate to `private static` methods. In JNI, these are `native` methods resolved by the JVM. In FFM, these become regular static methods that invoke a `MethodHandle`. The calling code does not change. - ---- - -## Implementation Plan - -### Phase 1: Core FFM Module & Code Parser - -#### Task 1.1 — Create `jParser/jParser-ffm` Gradle Module - -Create a new module at `jParser/jParser-ffm/` parallel to `jParser-cpp` and `jParser-teavm`. - -**Files to create:** -- `jParser/jParser-ffm/build.gradle.kts` - -```kotlin -plugins { - id("java-library") -} - -val moduleName = "jParser-ffm" - -dependencies { - implementation(project(":jParser:jParser-idl")) - implementation(project(":jParser:jParser-core")) - implementation(project(":idl:idl-core")) - - testImplementation(project(":loader:loader-core")) - testImplementation("junit:junit:${LibExt.jUnitVersion}") -} - -java { - sourceCompatibility = JavaVersion.toVersion("22") - targetCompatibility = JavaVersion.toVersion("22") -} - -tasks.withType { - options.compilerArgs.addAll(listOf("--enable-preview")) -} - -java { - withJavadocJar() - withSourcesJar() -} - -publishing { - publications { - create("maven") { - artifactId = moduleName - group = LibExt.groupId - version = LibExt.libVersion - from(components["java"]) - } - } -} -``` - -**Files to modify:** -- `settings.gradle.kts` — Add `include(":jParser:jParser-ffm")` -- `buildSrc/src/main/kotlin/publish.gradle.kts` — Add `project(":jParser:jParser-ffm")` to `libProjects` -- `jParser/jParser-build-tool/build.gradle.kts` — Add `implementation(project(":jParser:jParser-ffm"))` - -#### Task 1.2 — Implement `FFMCodeParser` - -Create `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java`. - -This class extends `IDLDefaultCodeParser` with header `"FFM"` and overrides the same hooks as `CppCodeParser`: - -| Hook | JNI (CppCodeParser) | FFM (FFMCodeParser) | -|---|---|---| -| `onIDLConstructorGenerated` | `private static native long internal_native_ClassName(...)` + `[-JNI;-NATIVE]` C++ block | `private static long internal_native_ClassName(...)` (non-native) + `MethodHandle` invocation body | -| `onIDLDeConstructorGenerated` | `private static native void internal_native_dispose(long this_addr)` | Same signature but non-native with `MethodHandle` call | -| `onIDLMethodGenerated` | `private static native internal_native_method(long this_addr, ...)` | Non-native method invoking `MethodHandle` | -| `onIDLAttributeGenerated` | `private static native internal_native_get/set_attr(long this_addr, ...)` | Non-native method invoking `MethodHandle` | -| `onIDLEnumMethodGenerated` | `private static native long internal_native_enumValue()` | Non-native method invoking `MethodHandle` | -| `setJavaBodyNativeCMD` | Passes content to `NativeCPPGenerator` | Passes content to `FFMCppGenerator` | -| `parseCodeBlock` (with `CMD_NATIVE`) | Sends C++ to `NativeCPPGenerator` | Sends C++ to `FFMCppGenerator` | - -**Key design decisions:** - -1. **MethodHandle storage**: Each class gets a private inner class `FFMHandles` with static `MethodHandle` fields, one per native function. These are initialized in a static block. - -2. **Bridge method pattern** (what each `internal_native_*` method looks like): -```java -// JNI version (current): -private static native long internal_native_MyClass(int param1); - -// FFM version (new): -private static long internal_native_MyClass(int param1) { - try { - return (long) FFMHandles.internal_native_MyClass.invokeExact(param1); - } catch(Throwable e) { - throw new RuntimeException(e); - } -} -``` - -3. **Static initializer block** (injected in `onParserComplete`): -```java -private static final class FFMHandles { - private static final SymbolLookup LOOKUP; - static { - LOOKUP = SymbolLookup.libraryLookup(System.mapLibraryName("MyLib64"), Arena.global()); - } - - static final MethodHandle internal_native_MyClass = LOOKUP.find("jparser_pkg_MyClass_internal_native_MyClass") - .map(addr -> Linker.nativeLinker().downcallHandle(addr, - FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT))) - .orElseThrow(); - // ... more handles -} -``` - -4. **FFM type mapping** (Java ↔ `ValueLayout`): - -| Java Type | ValueLayout | C Type | -|---|---|---| -| `long` | `JAVA_LONG` | `int64_t` | -| `int` | `JAVA_INT` | `int32_t` | -| `float` | `JAVA_FLOAT` | `float` | -| `double` | `JAVA_DOUBLE` | `double` | -| `boolean` | `JAVA_BOOLEAN` | `int32_t` (0/1) | -| `short` | `JAVA_SHORT` | `int16_t` | -| `byte` | `JAVA_BYTE` | `int8_t` | -| `void` | (none) | `void` | -| `String` | `ADDRESS` | `const char*` (via `Arena.ofAuto()` for allocation) | - -5. **String handling**: JNI automatically converts `jstring` ↔ `char*` via `GetStringUTFChars`/`NewStringUTF`. In FFM: - - **Outgoing strings**: Allocate a native segment with `Arena.ofConfined()`, copy the Java string bytes, pass the address. Free after call. - - **Return strings**: Read from the returned `MemorySegment` using `segment.getString(0)`. - - Generate string marshalling code directly in the Java bridge method body. - -#### Task 1.3 — Implement `FFMCppGenerator` - -Create `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java`. - -Implements the `CppGenerator` interface (or a new parallel interface if decoupling is needed). - -**Key differences from `NativeCPPGenerator`:** - -| Aspect | NativeCPPGenerator (JNI) | FFMCppGenerator (FFM) | -|---|---|---| -| Function signatures | `JNIEXPORT jlong JNICALL Java_com_pkg_Class_method(JNIEnv* env, jclass clazz, jlong this_addr)` | `extern "C" EXPORT int64_t jparser_com_pkg_Class_method(int64_t this_addr)` | -| Types | `jlong`, `jint`, `jfloat`, `jstring`, `jobject` | `int64_t`, `int32_t`, `float`, `const char*`, `void*` | -| String handling | `env->GetStringUTFChars(str, 0)` / `env->ReleaseStringUTFChars(str, ptr)` | Strings arrive as `const char*` directly — no conversion needed | -| Return objects | `return (jlong)ptr;` | `return (int64_t)ptr;` | -| Export macro | `JNIEXPORT` (JNI header) | Custom `EXPORT` macro: `__declspec(dllexport)` on Windows, `__attribute__((visibility("default")))` on Unix | -| Output file | `JNIGlue.cpp` + `JNIGlue.h` | `FFMGlue.cpp` + `FFMGlue.h` | -| Headers needed | `#include ` | `#include ` | - -**Generated C++ template example:** -```cpp -// FFMGlue.h -#pragma once -#include - -#ifdef _WIN32 - #define EXPORT __declspec(dllexport) -#else - #define EXPORT __attribute__((visibility("default"))) -#endif - -extern "C" { - -// Constructor -EXPORT int64_t jparser_com_pkg_MyClass_internal_native_MyClass(int32_t param1) { - return (int64_t)new MyClass((int)param1); -} - -// Method -EXPORT int64_t jparser_com_pkg_MyClass_internal_native_getValue(int64_t this_addr) { - MyClass* nativeObject = (MyClass*)this_addr; - return (int64_t)nativeObject->getValue(); -} - -// Destructor -EXPORT void jparser_com_pkg_MyClass_internal_native_dispose(int64_t this_addr) { - MyClass* nativeObject = (MyClass*)this_addr; - delete nativeObject; -} - -} // extern "C" -``` - -**Function naming convention:** `jparser___[__]` - -This differs from JNI naming (`Java_com_pkg_Class_method__JI`) but follows a similar pattern. The names must match exactly between: -- The C++ function name in `FFMGlue.cpp` -- The `SymbolLookup.find("...")` string in the generated Java code - -#### Task 1.4 — Implement `FFMMethodHandleRegistry` - -Create a utility class that `FFMCodeParser` uses during `onParserComplete` to inject the `FFMHandles` inner class: - -`jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java` - -This tracks all native functions discovered during parsing and, at the end, generates: -1. The `FFMHandles` inner class per Java class -2. The correct `FunctionDescriptor` for each method -3. The `SymbolLookup` initialization - -The registry accumulates entries like: -```java -class FFMEntry { - String symbolName; // "jparser_com_pkg_Class_method" - String javaMethodName; // "internal_native_method" - Type returnType; // from JavaParser AST - List parameterTypes; // from JavaParser AST - boolean isStatic; -} -``` - -### Phase 2: Build Integration - -#### Task 2.1 — Extend `BuildToolOptions` - -In `jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java`: - -- Add `public boolean generateFFM = false;` -- Add `private String moduleFFMPath;` computed as `modulePath + "/" + modulePrefix + "-ffm"` -- Add getter `getModuleFFMPath()` -- In `setup()`, compute `moduleFFMPath` like `moduleCorePath` and `moduleTeavmPath` - -#### Task 2.2 — Extend `BuilderTool` - -In `jParser/jParser-build-tool/src/main/java/com/github/xpenatan/jParser/builder/tool/BuilderTool.java`: - -Add a third generation block in `generateAndBuild()`: - -```java -if(op.generateFFM) { - FFMCppGenerator ffmGenerator = new FFMCppGenerator(op.getCPPDestinationPath()); - FFMCodeParser ffmParser = new FFMCodeParser(ffmGenerator, idlReader, op.packageName, op.getSourceDir()); - ffmParser.generateClass = true; - ffmParser.idlRenaming = packageRenaming; - JParser.generate(ffmParser, op.getModuleBaseJavaDir(), op.getModuleFFMPath() + "/src/main/java"); -} -``` - -#### Task 2.3 — FFM-Specific Build Targets - -The C++ compilation for FFM differs from JNI: - -- **No JNI headers required** — No `addJNIHeaders()` call -- **Different glue code** — Links `FFMGlue.cpp` instead of `JNIGlue.cpp` -- **Same shared library output** — `.dll` / `.so` / `.dylib` (same as JNI) -- **Exported symbols** — Must use `extern "C"` + visibility attributes - -Create convenience method in `DefaultBuildTarget`: -```java -public void addFFMGlueCode(String libBuildCPPPath) { - cppInclude.add(libBuildCPPPath + "/src/ffmglue/FFMGlue.cpp"); -} -``` - -### Phase 3: Runtime & Downstream Module Convention - -#### Task 3.1 — Library Loader for FFM - -Create `loader/loader-ffm/` module with `FFMLibraryLoader.java`: - -```java -public class FFMLibraryLoader { - private static SymbolLookup lookup; - - public static SymbolLookup load(String libraryName, String path) { - String fullPath = path + "/" + System.mapLibraryName(libraryName + "64"); - lookup = SymbolLookup.libraryLookup(Path.of(fullPath), Arena.global()); - return lookup; - } - - public static SymbolLookup getLookup() { - return lookup; - } -} -``` - -Alternatively, keep loading simple and let each generated class handle its own `SymbolLookup` — but a shared loader avoids loading the library multiple times. - -**Recommended approach**: A single `SymbolLookup` per library, initialized by the loader, and referenced by all generated classes. - -#### Task 3.2 — Downstream `lib-ffm` Module Convention - -For each library using jParser (e.g., `examples/TestLib`): - -``` -lib/ - lib-base/ (hand-written source with code blocks - unchanged) - lib-build/ (BuildLib.java - add FFM generation flag) - lib-core/ (generated JNI Java - unchanged) - lib-teavm/ (generated TeaVM Java - unchanged) - lib-ffm/ (NEW: generated FFM Java) - lib-desktop/ (runtime loader + packaged native libs - reused for FFM) -``` - -The `lib-ffm` module: -- Depends on `idl-core` (for `IDLBase`) -- Depends on `loader-ffm` (or `loader-core` if extending it) -- Contains generated Java with `MethodHandle`-based bridge methods -- Java 22+ target - -#### Task 3.3 — Example: Update `TestLib` BuildLib - -In `examples/TestLib/lib/lib-build/src/main/java/BuildLib.java`, add FFM handling: - -```java -if(op.containsArg("ffm")) { - op.generateFFM = true; - // FFM reuses the same native compilation targets but with FFMGlue -} -``` - -### Phase 4: Advanced Features (Deferred) - -#### Task 4.1 — Callback Support via `upcallStub` - -JNI callbacks use `env->CallVoidMethod(obj, methodID, ...)` to call back into Java. FFM requires: - -```java -MethodHandle target = MethodHandles.lookup().findVirtual(CallbackClass.class, "onEvent", ...); -MemorySegment upcallStub = Linker.nativeLinker().upcallStub(target, descriptor, arena); -// Pass upcallStub.address() to C++ as a function pointer -``` - -**This is architecturally different from JNI callbacks** and requires: -- Generating `upcallStub` creation code -- The C++ callback class must call a function pointer instead of `env->CallVoidMethod` -- Lifecycle management of the `Arena` backing the upcall stub - -**Recommendation**: Defer callbacks to Phase 4. Most FFM performance gains come from method calls, not callbacks. - -#### Task 4.2 — Array/Buffer Optimization - -FFM can avoid copying array data between Java and native: -- `MemorySegment.ofArray(array)` gives direct access to Java array memory -- `MemorySegment.ofBuffer(byteBuffer)` for direct buffers - -This is a performance optimization opportunity beyond what JNI provides (JNI requires `GetArrayElements` / `ReleaseArrayElements`). - -#### Task 4.3 — `[-FFM;-NATIVE]` Code Block Support - -In `lib-base` source, users can embed FFM-specific code blocks: - -```java -/*[-FFM;-NATIVE] - // Custom FFM-specific bridge code -*/ -``` - -`FFMCodeParser.setJavaBodyNativeCMD()` would handle this, similar to how `CppCodeParser` handles `[-JNI;-NATIVE]`. - ---- - -## File Change Summary - -### New Files - -| File | Purpose | -|---|---| -| `jParser/jParser-ffm/build.gradle.kts` | Module build config (Java 22+) | -| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java` | Main FFM code parser | -| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCppGenerator.java` | C glue code generator (extern "C") | -| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java` | Tracks MethodHandle entries per class | -| `jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java` | Maps Java types to ValueLayout and C types | -| `jParser/jParser-ffm/src/test/java/com/github/xpenatan/jParser/ffm/FFMCodeParserTest.java` | Unit tests | -| `loader/loader-ffm/build.gradle.kts` | FFM library loader module | -| `loader/loader-ffm/src/main/java/com/github/xpenatan/jParser/loader/FFMLibraryLoader.java` | Runtime loader using SymbolLookup | - -### Modified Files - -| File | Change | -|---|---| -| `settings.gradle.kts` | Add `include(":jParser:jParser-ffm")` and `include(":loader:loader-ffm")` | -| `buildSrc/src/main/kotlin/publish.gradle.kts` | Add both new modules to `libProjects` | -| `jParser/jParser-build-tool/build.gradle.kts` | Add dependency on `jParser-ffm` | -| `jParser/jParser-build-tool/src/main/java/.../BuilderTool.java` | Add FFM generation block | -| `jParser/jParser-build/src/main/java/.../tool/BuildToolOptions.java` | Add `generateFFM`, `moduleFFMPath` | -| `jParser/jParser-build/src/main/java/.../DefaultBuildTarget.java` | Add `addFFMGlueCode()` helper | - -### Downstream Library Changes (per library) - -| File | Change | -|---|---| -| `lib/lib-build/build.gradle.kts` | Add FFM-specific Gradle tasks | -| `lib/lib-build/src/main/java/BuildLib.java` | Add FFM target handling | -| `lib/lib-ffm/build.gradle.kts` | New module (generated output, Java 22+) | - ---- - -## Execution Order - -``` -Phase 1 (Core - do first): - 1.1 Create jParser-ffm module skeleton - 1.2 Implement FFMCodeParser - 1.3 Implement FFMCppGenerator - 1.4 Implement FFMMethodHandleRegistry + FFMTypeMapper - -Phase 2 (Build integration): - 2.1 Extend BuildToolOptions - 2.2 Extend BuilderTool - 2.3 Add FFM build target helpers - -Phase 3 (Runtime & examples): - 3.1 Create loader-ffm module - 3.2 Establish lib-ffm convention - 3.3 Update TestLib example - -Phase 4 (Advanced - deferred): - 4.1 Callback support via upcallStub - 4.2 Array/Buffer optimization - 4.3 [-FFM;-NATIVE] code block support -``` - ---- - -## Key Design Decisions - -1. **Java version**: Target Java 24 where FFM is stable (no `--enable-preview` needed). The `jParser-ffm` module has a higher Java target than the rest of the project (Java 11). - -2. **Coexistence**: `lib-core` (JNI) and `lib-ffm` (FFM) exist side-by-side. Downstream projects choose which to depend on. The public API is identical — only the internal bridge differs. This means a user's application code (`app/core`) can switch from JNI to FFM by changing a single dependency. - -3. **Symbol naming**: Use `jparser___` convention for C function names. This avoids JNI's complex mangling and is easier to debug. The naming must be deterministic and match between `FFMCppGenerator` and `FFMCodeParser`. - -4. **No `native` keyword**: FFM methods are regular static Java methods. The `native` keyword is not used. This means `DefaultCodeParser.CMD_NATIVE` handling needs a slight adaptation — for FFM, when a `[-FFM;-NATIVE]` block is encountered on a method, the C++ code is collected by `FFMCppGenerator` but the Java method gets a `MethodHandle.invokeExact()` body instead of being marked `native`. - -5. **Library loading strategy**: Single `SymbolLookup` per native library, stored in a generated utility class or provided by `loader-ffm`. All `MethodHandle` fields reference this shared lookup. The lookup is created once when the library is first loaded. - -6. **Error handling**: `MethodHandle.invokeExact()` throws checked `Throwable`. Each bridge method wraps this in an unchecked `RuntimeException`. This matches JNI behavior where native crashes propagate as runtime exceptions. - - diff --git a/LOCAL_AGENT.md b/LOCAL_AGENT.md deleted file mode 100644 index b07af6f3..00000000 --- a/LOCAL_AGENT.md +++ /dev/null @@ -1,35 +0,0 @@ -# LOCAL_AGENT.md — Session State - -## Current Task -FFM (Foreign Function & Memory API) — Phase 4: Advanced Features — **COMPLETE** - -## Current Progress -- [x] Phase 1 complete (all tasks 1.1–1.4) -- [x] Phase 2 complete (all tasks 2.1–2.3) -- [x] Phase 3 complete (all tasks 3.1–3.4) -- [x] **Task 4.1**: Callback support via `upcallStub` in `FFMCodeParser` -- [x] **Task 4.2**: Array/Buffer optimization helpers in `FFMTypeMapper` -- [x] **Task 4.3**: `[-FFM;-NATIVE]` code block support — verified working via existing pipeline -- [x] Compilation verified: `jParser-ffm`, `jParser-build-tool` both BUILD SUCCESSFUL -- [x] Existing tests pass (`jParser-idl:test`) - -## All Phases Complete - -All 4 phases of the FFM implementation plan are now complete: -- **Phase 1**: Core FFM module & code parser (`FFMCodeParser`, `FFMCppGenerator`, `FFMMethodHandleRegistry`, `FFMTypeMapper`) -- **Phase 2**: Build integration (`BuildToolOptions.generateFFM`, `BuilderTool` FFM block, `DefaultBuildTarget.addFFMGlueCode()`) -- **Phase 3**: Runtime & downstream (`TestLib lib-ffm` module, `BuildLib` FFM targets, Gradle tasks) -- **Phase 4**: Advanced features (callback via `upcallStub`, array/buffer helpers, `[-FFM;-NATIVE]` blocks) - -## Files Modified (Phase 4) -- `jParser/jParser-ffm/src/main/java/.../FFMCodeParser.java` — Added `onIDLCallbackGenerated()` + 7 helper methods for C++ callback class generation and upcall stub creation -- `jParser/jParser-ffm/src/main/java/.../FFMCppGenerator.java` — Added `addCallbackClassCode()` method -- `jParser/jParser-ffm/src/main/java/.../FFMNativeCodeGenerator.java` — Added `addCallbackClassCode()` to interface -- `jParser/jParser-ffm/src/main/java/.../FFMTypeMapper.java` — Added array type mappings + `getArraySegmentCode()`, `getBufferSegmentCode()`, `getArrayElementLayout()`, `isArrayType()` helpers - -## Key Design Decisions (Phase 4) -- **Function pointers instead of JNI callbacks**: C++ callback class stores typed function pointers instead of JNIEnv*/jobject/jmethodID. Virtual methods call function pointers directly. -- **upcallStub with Arena.ofAuto()**: GC-managed arena means stubs are freed when the Java callback is garbage-collected. No manual lifecycle management. -- **Static native setupCallback**: FFM uses explicit `this_addr` param (static method), unlike JNI which uses non-static native with implicit env/obj. -- **Array types map to ValueLayout.ADDRESS**: Arrays are passed as MemorySegment pointers, with C types like `int32_t*`, `float*`, etc. -- **[-FFM;-NATIVE] already works**: The existing pipeline (DefaultCodeParser.parseCodeBlock → CMD_NATIVE → setJavaBodyNativeCMD) handles it correctly for native methods with the FFM header. diff --git a/README.md b/README.md index 486cfc4b..f719d911 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Inspired by [gdx-jnigen](https://github.com/libgdx/gdx-jnigen), jParser allows y For web applications, jParser requires Emscripten to produce JS/WASM files and utilizes [TeaVM](https://github.com/konsoletyper/teavm). The classes generated in the TeaVM module use `JSBody` annotation solution to interact with JavaScript. -Currently, jParser supports only `JNI` and `TEAVM` code targets. There are plans to support the Java Foreign Function and Memory API (FFM). +Currently, jParser supports only `JNI`, `FFM` and `TEAVM` code targets. ## How it Works jParser consists of two main components: diff --git a/buildSrc/src/main/kotlin/LibExt.kt b/buildSrc/src/main/kotlin/LibExt.kt index 57c0c260..efba2e29 100644 --- a/buildSrc/src/main/kotlin/LibExt.kt +++ b/buildSrc/src/main/kotlin/LibExt.kt @@ -12,6 +12,7 @@ object LibExt { const val java8Target = "1.8" const val java11Target = "11" + const val java24Target = "24" // Lib Dependencies const val jniGenVersion = "2.5.1" diff --git a/buildSrc/src/main/kotlin/publish.gradle.kts b/buildSrc/src/main/kotlin/publish.gradle.kts index 9b209cd4..d08a30f3 100644 --- a/buildSrc/src/main/kotlin/publish.gradle.kts +++ b/buildSrc/src/main/kotlin/publish.gradle.kts @@ -17,6 +17,7 @@ var libProjects = mutableSetOf( project(":idl-helper:idl-helper-core"), project(":idl-helper:idl-helper-teavm"), project(":idl-helper:idl-helper-desktop"), + project(":idl-helper:idl-helper-ffm"), project(":idl-helper:idl-helper-android"), project(":loader:loader-core"), project(":loader:loader-teavm"), diff --git a/examples/SharedLib/app/core/build.gradle.kts b/examples/SharedLib/app/core/build.gradle.kts index d0c13e5a..253d7662 100644 --- a/examples/SharedLib/app/core/build.gradle.kts +++ b/examples/SharedLib/app/core/build.gradle.kts @@ -8,8 +8,13 @@ java { } dependencies { - implementation(project(":examples:SharedLib:libA:lib-core")) - implementation(project(":examples:SharedLib:libB:lib-core")) + // compileOnly: app/core compiles against lib-core's API, but does NOT + // propagate it transitively. Each platform module (desktop, android, teavm) + // provides the actual native bridge implementation: + // - lib-core for JNI (desktop/android) + // - lib-ffm for FFM (desktop with Java 22+) + compileOnly(project(":examples:SharedLib:libA:lib-core")) + compileOnly(project(":examples:SharedLib:libB:lib-core")) implementation("com.badlogicgames.gdx:gdx:${LibExt.gdxVersion}") diff --git a/examples/SharedLib/app/desktop/build.gradle.kts b/examples/SharedLib/app/desktop/build.gradle.kts index d234ee39..b39b8fcf 100644 --- a/examples/SharedLib/app/desktop/build.gradle.kts +++ b/examples/SharedLib/app/desktop/build.gradle.kts @@ -11,6 +11,13 @@ java { dependencies { implementation(project(":examples:SharedLib:app:core")) + + // Choose ONE native bridge per lib — lib-core (JNI) or lib-ffm (FFM). + // implementation(project(":examples:SharedLib:libA:lib-core")) // JNI (default) + // implementation(project(":examples:SharedLib:libB:lib-core")) // JNI (default) + implementation(project(":examples:SharedLib:libA:lib-ffm")) // FFM (Java 22+, no JNI overhead) + implementation(project(":examples:SharedLib:libB:lib-ffm")) // FFM (Java 22+, no JNI overhead) + implementation(project(":examples:SharedLib:libA:lib-desktop")) implementation(project(":examples:SharedLib:libB:lib-desktop")) implementation(project(":idl-helper:idl-helper-desktop")) @@ -25,6 +32,12 @@ tasks.register("SharedLib_run_app_desktop") { mainClass.set("com.github.xpenatan.jParser.example.app.Main") classpath = sourceSets["main"].runtimeClasspath + // FFM requires JDK 22+ and native access + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + jvmArgs("--enable-native-access=ALL-UNNAMED") + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { jvmArgs("-XstartOnFirstThread") } diff --git a/examples/SharedLib/libA/lib-base/src/main/java/libA/LibALoader.java b/examples/SharedLib/libA/lib-base/src/main/java/libA/LibALoader.java index eb2f0d44..47f864b2 100644 --- a/examples/SharedLib/libA/lib-base/src/main/java/libA/LibALoader.java +++ b/examples/SharedLib/libA/lib-base/src/main/java/libA/LibALoader.java @@ -11,6 +11,10 @@ public class LibALoader { #include "LibACustomCode.h" */ + /*[-FFM;-NATIVE] + #include "LibACustomCode.h" + */ + public static void init(JParserLibraryLoaderListener listener) { JParserLibraryLoader.load(LIB_NAME, listener); } diff --git a/examples/SharedLib/libA/lib-build/build.gradle.kts b/examples/SharedLib/libA/lib-build/build.gradle.kts index bbbd310e..72a311bd 100644 --- a/examples/SharedLib/libA/lib-build/build.gradle.kts +++ b/examples/SharedLib/libA/lib-build/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(project(":jParser:jParser-cpp")) implementation(project(":jParser:jParser-build")) implementation(project(":jParser:jParser-build-tool")) + implementation(project(":jParser:jParser-ffm")) } implementation(project(":idl-helper:idl-helper-core")) @@ -102,4 +103,46 @@ tasks.register("LibA_build_project_ios") { mainClass.set(mainClassName) args = mutableListOf("ios") classpath = sourceSets["main"].runtimeClasspath -} \ No newline at end of file +} + +// FFM tasks — generate FFM Java code and/or compile native libs with FFMGlue + +tasks.register("LibA_build_project_ffm") { + group = "lib" + description = "Generate FFM Java code only (no native compilation)" + mainClass.set(mainClassName) + args = mutableListOf("ffm") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibA_build_project_ffm_windows64") { + group = "lib" + description = "Generate FFM Java code and compile for Windows with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_windows64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibA_build_project_ffm_linux64") { + group = "lib" + description = "Generate FFM Java code and compile for Linux with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_linux64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibA_build_project_ffm_mac64") { + group = "lib" + description = "Generate FFM Java code and compile for Mac with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_mac64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibA_build_project_ffm_macArm") { + group = "lib" + description = "Generate FFM Java code and compile for Mac ARM with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_macArm") + classpath = sourceSets["main"].runtimeClasspath +} diff --git a/examples/SharedLib/libA/lib-build/src/main/java/BuildLibA.java b/examples/SharedLib/libA/lib-build/src/main/java/BuildLibA.java index 1dd0dc2c..3374cbe0 100644 --- a/examples/SharedLib/libA/lib-build/src/main/java/BuildLibA.java +++ b/examples/SharedLib/libA/lib-build/src/main/java/BuildLibA.java @@ -36,6 +36,12 @@ public static void main(String[] args) throws Exception { BuildToolOptions op = new BuildToolOptions(data, args); op.addAdditionalIDLRefPath(IDLReader.getIDLHelperFile()); + + // Enable FFM code generation if requested + if(op.containsArg("ffm")) { + op.generateFFM = true; + } + BuilderTool.build(op, new BuildToolListener() { @Override public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList targets) { @@ -61,6 +67,20 @@ public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList/ffm/ to avoid conflicts with JNI libs. + + private static BuildMultiTarget getFFMWindowVCTarget(BuildToolOptions op) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String config = "/DLIB_USER_CONFIG=\"\\\"LibACustomConfig.h\\\"\""; + + // Make a static library + WindowsMSVCTarget compileStaticTarget = new WindowsMSVCTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "windows/vc/ffm"; + compileStaticTarget.cppFlags.add("/std:c++11"); + compileStaticTarget.cppFlags.add("/DLIBA_EXPORTS"); + compileStaticTarget.cppFlags.add(config); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + WindowsMSVCTarget linkTarget = new WindowsMSVCTarget(); + linkTarget.libDirSuffix = "windows/vc/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("/std:c++11"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("/WHOLEARCHIVE:" + libBuildCPPPath + "/libs/windows/vc/ffm/" + op.libName + "64_.lib"); + linkTarget.linkerFlags.add("-DLL"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMLinuxTarget(BuildToolOptions op) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String config = "-DLIB_USER_CONFIG=\"LibACustomConfig.h\""; + + // Make a static library + LinuxTarget compileStaticTarget = new LinuxTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "linux/ffm"; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add(config); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.cppFlags.add("-fvisibility=hidden"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + LinuxTarget linkTarget = new LinuxTarget(); + linkTarget.libDirSuffix = "linux/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add(config); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.cppFlags.add("-fvisibility=hidden"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("-Wl,-soname,libLibA64.so"); + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/linux/ffm/lib" + op.libName + "64_.a"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMMacTarget(BuildToolOptions op, boolean isArm) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String config = "-DLIB_USER_CONFIG=\"LibACustomConfig.h\""; + String macSubDir = isArm ? "mac/arm/ffm" : "mac/ffm"; + + // Make a static library + MacTarget compileStaticTarget = new MacTarget(isArm); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = macSubDir; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add(config); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + MacTarget linkTarget = new MacTarget(isArm); + linkTarget.libDirSuffix = macSubDir; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add(config); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("-Wl,-force_load"); + if(isArm) { + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/arm/ffm/lib" + op.libName + "64_.a"); + } + else { + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/ffm/lib" + op.libName + "64_.a"); + } + multiTarget.add(linkTarget); + + return multiTarget; + } } \ No newline at end of file diff --git a/examples/SharedLib/libA/lib-desktop/build.gradle.kts b/examples/SharedLib/libA/lib-desktop/build.gradle.kts index 5e43f43b..39fdb553 100644 --- a/examples/SharedLib/libA/lib-desktop/build.gradle.kts +++ b/examples/SharedLib/libA/lib-desktop/build.gradle.kts @@ -9,7 +9,8 @@ java { val libDir = "${projectDir}/../lib-build/build/c++/libs" //val windowsFile = "$libDir/windows/LibA64.dll" -val windowsFile = "$libDir/windows/vc/LibA64.dll" +//val windowsFile = "$libDir/windows/vc/LibA64.dll" +val windowsFile = "$libDir/windows/vc/ffm/LibA64.dll" val linuxFile = "$libDir/linux/libLibA64.so" val macFile = "$libDir/mac/libLibA64.dylib" val macArmFile = "$libDir/mac/arm/libLibAarm64.dylib" diff --git a/examples/SharedLib/libA/lib-ffm/build.gradle.kts b/examples/SharedLib/libA/lib-ffm/build.gradle.kts new file mode 100644 index 00000000..91ec2327 --- /dev/null +++ b/examples/SharedLib/libA/lib-ffm/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("java") + id("java-library") +} + +// FFM (java.lang.foreign.*) requires JDK 22+ at runtime. +// We compile with JDK 25 to access the FFM API, but set targetCompatibility +// to Java 11 so Gradle's dependency metadata stays compatible with lower-JVM +// consumer modules. The --release flag is cleared so the JDK 25 API is +// available despite the Java 11 bytecode target. +// It is the consumer's responsibility to run the application on JDK 22+. +java { + sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) +} + +tasks.withType { + javaCompiler.set(javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + // Clear --release so JDK 25 APIs (java.lang.foreign) are accessible + // even with -source 11 -target 11 bytecode output. + options.release.set(null as Int?) +} + +dependencies { + if(LibExt.exampleUseRepoLibs) { + api("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-core:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-helper-ffm:-SNAPSHOT") + } + else { + api(project(":loader:loader-core")) + api(project(":idl:idl-core")) + api(project(":idl-helper:idl-helper-ffm")) + } +} + +tasks.named("clean") { + doFirst { + val srcPath = "$projectDir/src/main/" + project.delete(files(srcPath)) + } +} + diff --git a/examples/SharedLib/libB/lib-base/src/main/java/libB/LibBLoader.java b/examples/SharedLib/libB/lib-base/src/main/java/libB/LibBLoader.java index faeb11e1..eb40c732 100644 --- a/examples/SharedLib/libB/lib-base/src/main/java/libB/LibBLoader.java +++ b/examples/SharedLib/libB/lib-base/src/main/java/libB/LibBLoader.java @@ -9,6 +9,10 @@ public class LibBLoader { #include "LibBCustomCode.h" */ + /*[-FFM;-NATIVE] + #include "LibBCustomCode.h" + */ + public static final String LIB_NAME = "LibB"; public static void init(JParserLibraryLoaderListener listener) { diff --git a/examples/SharedLib/libB/lib-build/build.gradle.kts b/examples/SharedLib/libB/lib-build/build.gradle.kts index 7470e797..8dcdf9a3 100644 --- a/examples/SharedLib/libB/lib-build/build.gradle.kts +++ b/examples/SharedLib/libB/lib-build/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { implementation(project(":jParser:jParser-cpp")) implementation(project(":jParser:jParser-build")) implementation(project(":jParser:jParser-build-tool")) + implementation(project(":jParser:jParser-ffm")) } implementation(project(":idl-helper:idl-helper-core")) @@ -104,4 +105,46 @@ tasks.register("LibB_build_project_ios") { mainClass.set(mainClassName) args = mutableListOf("ios") classpath = sourceSets["main"].runtimeClasspath -} \ No newline at end of file +} + +// FFM tasks — generate FFM Java code and/or compile native libs with FFMGlue + +tasks.register("LibB_build_project_ffm") { + group = "lib" + description = "Generate FFM Java code only (no native compilation)" + mainClass.set(mainClassName) + args = mutableListOf("ffm") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibB_build_project_ffm_windows64") { + group = "lib" + description = "Generate FFM Java code and compile for Windows with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_windows64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibB_build_project_ffm_linux64") { + group = "lib" + description = "Generate FFM Java code and compile for Linux with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_linux64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibB_build_project_ffm_mac64") { + group = "lib" + description = "Generate FFM Java code and compile for Mac with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_mac64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("LibB_build_project_ffm_macArm") { + group = "lib" + description = "Generate FFM Java code and compile for Mac ARM with FFMGlue" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_macArm") + classpath = sourceSets["main"].runtimeClasspath +} diff --git a/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java b/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java index 4b0d6ea9..fc9c2471 100644 --- a/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java +++ b/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java @@ -42,6 +42,11 @@ public static void main(String[] args) throws Exception { op.addAdditionalIDLRefPath(IDLReader.parseFile(libAPath + "/lib-build/src/main/cpp/LibA.idl")); op.addAdditionalIDLRefPath(IDLReader.getIDLHelperFile()); + // Enable FFM code generation if requested + if(op.containsArg("ffm")) { + op.generateFFM = true; + } + BuilderTool.build(op, new BuildToolListener() { @Override public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList targets) { @@ -67,6 +72,20 @@ public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList/ffm/ to avoid conflicts with JNI libs. + + private static BuildMultiTarget getFFMWindowVCTarget(BuildToolOptions op, String libAPath) { + String libALibPath = libAPath + "/lib-build/build/c++/libs/windows/vc/ffm"; + String libACPPPath = libAPath + "/lib-build/src/main/cpp"; + String libASourcePath = libACPPPath + "/source"; + String libACustomPath = libACPPPath + "/custom"; + + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String config = "/DLIB_USER_CONFIG=\"\\\"LibACustomConfig.h\\\"\""; + + // Make a static library + WindowsMSVCTarget compileStaticTarget = new WindowsMSVCTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "windows/vc/ffm"; + compileStaticTarget.cppFlags.add("/std:c++11"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + libASourcePath); + compileStaticTarget.headerDirs.add("-I" + libACustomPath); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + WindowsMSVCTarget linkTarget = new WindowsMSVCTarget(); + linkTarget.libDirSuffix = "windows/vc/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("/std:c++11"); + linkTarget.cppFlags.add(config); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.headerDirs.add("-I" + libASourcePath); + linkTarget.headerDirs.add("-I" + libACustomPath); + linkTarget.linkerFlags.add("/WHOLEARCHIVE:" + libALibPath + "/LibA64.lib"); + linkTarget.linkerFlags.add("/WHOLEARCHIVE:" + libBuildCPPPath + "/libs/windows/vc/ffm/" + op.libName + "64_.lib"); + linkTarget.linkerFlags.add("-DLL"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMLinuxTarget(BuildToolOptions op, String libAPath) { + String libALibPath = libAPath + "/lib-build/build/c++/libs/linux/ffm"; + String libACPPPath = libAPath + "/lib-build/src/main/cpp"; + String libASourcePath = libACPPPath + "/source"; + String libACustomPath = libACPPPath + "/custom"; + + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String config = "-DLIB_USER_CONFIG=\"LibACustomConfig.h\""; + + // Make a static library + LinuxTarget compileStaticTarget = new LinuxTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "linux/ffm"; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add(config); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.cppFlags.add("-fvisibility=hidden"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + libASourcePath); + compileStaticTarget.headerDirs.add("-I" + libACustomPath); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + LinuxTarget linkTarget = new LinuxTarget(); + linkTarget.libDirSuffix = "linux/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add(config); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.cppFlags.add("-fvisibility=hidden"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.headerDirs.add("-I" + libASourcePath); + linkTarget.headerDirs.add("-I" + libACustomPath); + linkTarget.linkerFlags.add(libALibPath + "/libLibA64.so"); + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/linux/ffm/lib" + op.libName + "64_.a"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMMacTarget(BuildToolOptions op, boolean isArm, String libAPath) { + String libALibPath = libAPath + "/lib-build/build/c++/libs/mac/ffm"; + String libALibArmPath = libAPath + "/lib-build/build/c++/libs/mac/arm/ffm"; + String libACPPPath = libAPath + "/lib-build/src/main/cpp"; + String libASourcePath = libACPPPath + "/source"; + String libACustomPath = libACPPPath + "/custom"; + + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String sourceDir = op.getSourceDir(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String config = "-DLIB_USER_CONFIG=\"LibACustomConfig.h\""; + String macSubDir = isArm ? "mac/arm/ffm" : "mac/ffm"; + + // Make a static library + MacTarget compileStaticTarget = new MacTarget(isArm); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = macSubDir; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add(config); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.headerDirs.add("-I" + sourceDir); + compileStaticTarget.headerDirs.add("-I" + libASourcePath); + compileStaticTarget.headerDirs.add("-I" + libACustomPath); + compileStaticTarget.cppInclude.add(sourceDir + "**.cpp"); + multiTarget.add(compileStaticTarget); + + // Link with FFMGlue instead of JNIGlue — no JNI headers + MacTarget linkTarget = new MacTarget(isArm); + linkTarget.libDirSuffix = macSubDir; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add(config); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.headerDirs.add("-I" + sourceDir); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.headerDirs.add("-I" + libASourcePath); + linkTarget.headerDirs.add("-I" + libACustomPath); + + if(isArm) { + linkTarget.linkerFlags.add(libALibArmPath + "/libLibA64.dylib"); + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/arm/ffm/lib" + op.libName + "64_.a"); + } + else { + linkTarget.linkerFlags.add(libALibPath + "/libLibA64.dylib"); + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/ffm/lib" + op.libName + "64_.a"); + } + multiTarget.add(linkTarget); + + return multiTarget; + } } diff --git a/examples/SharedLib/libB/lib-desktop/build.gradle.kts b/examples/SharedLib/libB/lib-desktop/build.gradle.kts index e9480b55..6f106278 100644 --- a/examples/SharedLib/libB/lib-desktop/build.gradle.kts +++ b/examples/SharedLib/libB/lib-desktop/build.gradle.kts @@ -9,7 +9,8 @@ java { val libDir = "${projectDir}/../lib-build/build/c++/libs" //val windowsFile = "$libDir/windows/LibB64.dll" -val windowsFile = "$libDir/windows/vc/LibB64.dll" +//val windowsFile = "$libDir/windows/vc/LibB64.dll" +val windowsFile = "$libDir/windows/vc/ffm/LibB64.dll" val linuxFile = "$libDir/linux/libLibB64.so" val macFile = "$libDir/mac/libLibB64.dylib" val macArmFile = "$libDir/mac/arm/libLibBarm64.dylib" diff --git a/examples/SharedLib/libB/lib-ffm/build.gradle.kts b/examples/SharedLib/libB/lib-ffm/build.gradle.kts new file mode 100644 index 00000000..167fd184 --- /dev/null +++ b/examples/SharedLib/libB/lib-ffm/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + id("java") + id("java-library") +} +// FFM (java.lang.foreign.*) requires JDK 22+ at runtime. +java { + sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) +} +tasks.withType { + javaCompiler.set(javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + options.release.set(null as Int?) +} +dependencies { + implementation(project(":examples:SharedLib:libA:lib-ffm")) + if(LibExt.exampleUseRepoLibs) { + api("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-core:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-helper-ffm:-SNAPSHOT") + } + else { + api(project(":loader:loader-core")) + api(project(":idl:idl-core")) + api(project(":idl-helper:idl-helper-ffm")) + } +} +tasks.named("clean") { + doFirst { + val srcPath = "$projectDir/src/main/" + project.delete(files(srcPath)) + } +} diff --git a/examples/TestLib/app/android/build.gradle.kts b/examples/TestLib/app/android/build.gradle.kts index 8122fb03..13367c8f 100644 --- a/examples/TestLib/app/android/build.gradle.kts +++ b/examples/TestLib/app/android/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { natives("com.badlogicgames.gdx:gdx-platform:${LibExt.gdxVersion}:natives-x86") implementation(project(":examples:TestLib:app:core")) + implementation(project(":examples:TestLib:lib:lib-core")) implementation(project(":examples:TestLib:lib:lib-android")) } diff --git a/examples/TestLib/app/core/build.gradle.kts b/examples/TestLib/app/core/build.gradle.kts index 157ae626..6038fc91 100644 --- a/examples/TestLib/app/core/build.gradle.kts +++ b/examples/TestLib/app/core/build.gradle.kts @@ -8,7 +8,13 @@ java { } dependencies { - implementation(project(":examples:TestLib:lib:lib-core")) + // compileOnly: app/core compiles against lib-core's API, but does NOT + // propagate it transitively. Each platform module (desktop, android, teavm) + // provides the actual native bridge implementation: + // - lib-core for JNI (desktop/android) + // - lib-ffm for FFM (desktop with Java 22+) + // - lib-teavm for TeaVM (web) + compileOnly(project(":examples:TestLib:lib:lib-core")) implementation("com.badlogicgames.gdx:gdx:${LibExt.gdxVersion}") } \ No newline at end of file diff --git a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmark.java b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmark.java new file mode 100644 index 00000000..d3792427 --- /dev/null +++ b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmark.java @@ -0,0 +1,363 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.Application; +import com.badlogic.gdx.Gdx; +import com.github.xpenatan.jParser.example.testlib.TestAttributeClass; +import com.github.xpenatan.jParser.example.testlib.TestBufferManualClass; +import com.github.xpenatan.jParser.example.testlib.TestMethodClass; +import com.github.xpenatan.jParser.example.testlib.TestObjectClass; +import com.github.xpenatan.jparser.idl.helper.IDLIntArray; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; + +/** + * Comprehensive native bridge benchmark comparing JNI, FFM, and TeaVM performance. + *

+ * Each benchmark measures the cost of crossing the Java → native boundary for + * different parameter types. The native work is intentionally trivial so the + * measurement isolates bridge overhead, not native computation. + *

+ * Results are printed to stdout. Run this on desktop with either lib-core (JNI) + * or lib-ffm (FFM) on the classpath to compare. On TeaVM the iteration count + * is reduced automatically. + */ +public class NativeBridgeBenchmark { + + // --------------------------------------------------------------------------- + // Configuration + // --------------------------------------------------------------------------- + + /** Number of warm-up iterations (allows JIT to stabilise). */ + private static final int WARMUP_ITERATIONS = 2; + /** Number of timed iterations whose median is reported. */ + private static final int TIMED_ITERATIONS = 3; + + /** Calls per timed iteration – adjusted for TeaVM. */ + private static long CALLS_PER_ITERATION = 1_000_000L; + + // Reusable native objects – allocated once to avoid measuring allocation. + private static TestMethodClass methodObj; + private static TestObjectClass objectA; + private static TestObjectClass objectB; + private static TestAttributeClass attrObj; + private static TestBufferManualClass bufferObj; + private static IDLIntArray intArray; + private static ByteBuffer byteBuffer; + + /** Collected results for CSV export. */ + private static final ArrayList results = new ArrayList<>(); + + /** Simple data holder for one benchmark measurement. */ + public static class BenchmarkResult { + public final String label; + public final double medianMs; + public final double mcallsPerSec; + + public BenchmarkResult(String label, double medianMs, double mcallsPerSec) { + this.label = label; + this.medianMs = medianMs; + this.mcallsPerSec = mcallsPerSec; + } + } + + // --------------------------------------------------------------------------- + // Public entry point + // --------------------------------------------------------------------------- + + public static void run() { + if(Gdx.app != null && Gdx.app.getType() == Application.ApplicationType.WebGL) { + CALLS_PER_ITERATION = 500_000L; + } + + System.out.println("======================================================="); + System.out.println(" Native Bridge Benchmark"); + System.out.println(" Warm-up rounds : " + WARMUP_ITERATIONS); + System.out.println(" Timed rounds : " + TIMED_ITERATIONS); + System.out.println(" Calls/round : " + CALLS_PER_ITERATION); + System.out.println("======================================================="); + + results.clear(); + + allocateResources(); + try { + runAllBenchmarks(); + } finally { + freeResources(); + } + + System.out.println("======================================================="); + System.out.println(" Benchmark complete"); + System.out.println("======================================================="); + + // Write CSV if output path is specified via system property + String outputPath = System.getProperty("benchmark.output"); + if(outputPath != null && !outputPath.isEmpty()) { + writeCsv(outputPath); + } + } + + /** + * Returns the collected results from the last {@link #run()} call. + */ + public static ArrayList getResults() { + return results; + } + + /** + * Writes collected results to a CSV file. + */ + private static void writeCsv(String path) { + try(PrintWriter pw = new PrintWriter(new FileWriter(path))) { + pw.println("# warmup=" + WARMUP_ITERATIONS); + pw.println("# timed=" + TIMED_ITERATIONS); + pw.println("# calls=" + CALLS_PER_ITERATION); + pw.println("label,medianMs,mcallsPerSec"); + for(BenchmarkResult r : results) { + pw.printf("%s,%.4f,%.4f%n", r.label, r.medianMs, r.mcallsPerSec); + } + System.out.println(" Results written to: " + path); + } catch(IOException e) { + System.err.println(" Failed to write CSV: " + e.getMessage()); + } + } + + // --------------------------------------------------------------------------- + // Benchmark runner + // --------------------------------------------------------------------------- + + private static void runAllBenchmarks() { + benchmark("void(int) ", NativeBridgeBenchmark::benchSetInt); + benchmark("void(float, bool) ", NativeBridgeBenchmark::benchSetFloatBool); + benchmark("void(int,int,float,float,bool)", NativeBridgeBenchmark::benchSetManyPrimitives); + benchmark("int getter ", NativeBridgeBenchmark::benchGetInt); + benchmark("float getter ", NativeBridgeBenchmark::benchGetFloat); + benchmark("void(String) ", NativeBridgeBenchmark::benchSetString); + benchmark("String getter ", NativeBridgeBenchmark::benchGetString); + benchmark("void(Obj*,Obj*,Obj&,Obj&)", NativeBridgeBenchmark::benchObjectPassing); + benchmark("Object* getter ", NativeBridgeBenchmark::benchObjectReturn); + benchmark("attribute set int ", NativeBridgeBenchmark::benchAttrSetInt); + benchmark("attribute get int ", NativeBridgeBenchmark::benchAttrGetInt); + benchmark("attribute set float ", NativeBridgeBenchmark::benchAttrSetFloat); + benchmark("attribute get float ", NativeBridgeBenchmark::benchAttrGetFloat); + benchmark("IDLIntArray set+get ", NativeBridgeBenchmark::benchIDLIntArray); + benchmark("ByteBuffer update (256B)", NativeBridgeBenchmark::benchByteBuffer); + } + + /** + * Runs warm-up + timed iterations, then prints the median time and + * throughput (million calls/sec). + */ + private static void benchmark(String label, Runnable body) { + // Warm-up + for(int i = 0; i < WARMUP_ITERATIONS; i++) { + body.run(); + } + + // Timed + long[] times = new long[TIMED_ITERATIONS]; + for(int i = 0; i < TIMED_ITERATIONS; i++) { + long t0 = System.nanoTime(); + body.run(); + long t1 = System.nanoTime(); + times[i] = t1 - t0; + } + + // Sort for median + java.util.Arrays.sort(times); + long medianNs = times[TIMED_ITERATIONS / 2]; + double medianMs = medianNs / 1_000_000.0; + double mcallsPerSec = (CALLS_PER_ITERATION / (medianNs / 1_000_000_000.0)) / 1_000_000.0; + + // Trim label for display but keep original for CSV + String trimmed = label.trim(); + results.add(new BenchmarkResult(trimmed, medianMs, mcallsPerSec)); + + System.out.printf(" %-38s %8.1f ms %8.2f Mcalls/s%n", label, medianMs, mcallsPerSec); + } + + // --------------------------------------------------------------------------- + // Resource lifecycle + // --------------------------------------------------------------------------- + + private static void allocateResources() { + methodObj = new TestMethodClass(); + objectA = new TestObjectClass(); + objectB = new TestObjectClass(); + attrObj = new TestAttributeClass(); + bufferObj = new TestBufferManualClass(); + intArray = new IDLIntArray(64); + + byteBuffer = ByteBuffer.allocateDirect(256); + byteBuffer.order(ByteOrder.nativeOrder()); + + // Pre-populate so native reads valid data + objectA.set_intValue01(42); + objectA.set_floatValue01(3.14f); + objectB.set_intValue01(99); + objectB.set_floatValue01(2.71f); + methodObj.setMethod05("hello"); + for(int i = 0; i < 64; i++) { + intArray.setValue(i, i); + } + for(int i = 0; i < 256; i++) { + byteBuffer.put(i, (byte) i); + } + } + + private static void freeResources() { + intArray.dispose(); + attrObj.dispose(); + objectB.dispose(); + objectA.dispose(); + methodObj.dispose(); + // bufferObj has no C++ allocation to free (extends IDLBase with 0 addr) + } + + // --------------------------------------------------------------------------- + // Individual benchmarks + // --------------------------------------------------------------------------- + + /** Pass a single int primitive to native. */ + private static void benchSetInt() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + methodObj.setMethod01((int) i); + } + } + + /** Pass float + boolean to native. */ + private static void benchSetFloatBool() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + methodObj.setMethod02(1.5f, true); + } + } + + /** Pass 5 primitives (int, int, float, float, bool) in one call. */ + private static void benchSetManyPrimitives() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + methodObj.setMethod03(1, 2, 3.0f, 4.0f, true); + } + } + + /** Return a single int from native. */ + private static void benchGetInt() { + int sink = 0; + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + sink += methodObj.getIntValue01(); + } + preventOptimisation(sink); + } + + /** Return a single float from native. */ + private static void benchGetFloat() { + float sink = 0; + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + sink += methodObj.getFloatValue01(); + } + preventOptimisation(sink); + } + + /** Pass a Java String to native. */ + private static void benchSetString() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + methodObj.setMethod05("benchmark"); + } + } + + /** Return a String (via IDLString) from native. */ + private static void benchGetString() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + methodObj.getStrValue01().data(); + } + } + + /** Pass multiple object pointers and references to native. */ + private static void benchObjectPassing() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + methodObj.setMethod06(objectA, objectB, objectA, objectB); + } + } + + /** Return an object pointer from native. */ + private static void benchObjectReturn() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + methodObj.getPointerObject02(); + } + } + + /** Set an int attribute on a native object. */ + private static void benchAttrSetInt() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + attrObj.set_intValue01((int) i); + } + } + + /** Get an int attribute from a native object. */ + private static void benchAttrGetInt() { + int sink = 0; + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + sink += attrObj.get_intValue01(); + } + preventOptimisation(sink); + } + + /** Set a float attribute on a native object. */ + private static void benchAttrSetFloat() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + attrObj.set_floatValue01((float) i); + } + } + + /** Get a float attribute from a native object. */ + private static void benchAttrGetFloat() { + float sink = 0; + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + sink += attrObj.get_floatValue01(); + } + preventOptimisation(sink); + } + + /** Write + read IDLIntArray elements (2 native calls per iteration). */ + private static void benchIDLIntArray() { + int sink = 0; + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + int idx = (int) (i & 63); + intArray.setValue(idx, (int) i); + sink += intArray.getValue(idx); + } + preventOptimisation(sink); + } + + /** Pass a direct ByteBuffer to native for bulk update. */ + private static void benchByteBuffer() { + for(long i = 0; i < CALLS_PER_ITERATION; i++) { + bufferObj.updateByteBuffer(byteBuffer, 256, (byte) 0x42); + } + } + + // --------------------------------------------------------------------------- + // Anti-optimisation fence – prevents dead-code elimination + // --------------------------------------------------------------------------- + + private static volatile int intSink; + private static volatile float floatSink; + + private static void preventOptimisation(int value) { + intSink = value; + } + + private static void preventOptimisation(float value) { + floatSink = value; + } +} + + + + + + + + diff --git a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkApp.java b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkApp.java new file mode 100644 index 00000000..b9bfabdb --- /dev/null +++ b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkApp.java @@ -0,0 +1,34 @@ +package com.github.xpenatan.jParser.example.app; +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.ScreenUtils; +import com.github.xpenatan.jParser.example.testlib.TestLibLoader; +import com.github.xpenatan.jparser.idl.IDLLoader; +public class NativeBridgeBenchmarkApp extends ApplicationAdapter { + private boolean init = false; + @Override + public void create() { + IDLLoader.init((idl_isSuccess, idl_e) -> { + if(idl_e != null) { + idl_e.printStackTrace(); + return; + } + TestLibLoader.init((isSuccess, e) -> { + if(e != null) { + e.printStackTrace(); + } + init = isSuccess; + }); + }); + } + @Override + public void render() { + if(init) { + init = false; + NativeBridgeBenchmark.run(); + Gdx.app.exit(); + } + ScreenUtils.clear(Color.GREEN); + } +} diff --git a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkCompare.java b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkCompare.java new file mode 100644 index 00000000..c06f332a --- /dev/null +++ b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkCompare.java @@ -0,0 +1,191 @@ +package com.github.xpenatan.jParser.example.app; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Reads two benchmark CSV files (JNI and FFM) and prints a side-by-side + * comparison table to stdout, and optionally writes it to a file. + *

+ * Usage: {@code java NativeBridgeBenchmarkCompare [output.txt]} + */ +public class NativeBridgeBenchmarkCompare { + + private static class Row { + final String label; + final double medianMs; + final double mcallsPerSec; + + Row(String label, double medianMs, double mcallsPerSec) { + this.label = label; + this.medianMs = medianMs; + this.mcallsPerSec = mcallsPerSec; + } + } + + /** Holds parsed CSV rows together with optional metadata from comment lines. */ + private static class CsvData { + final Map rows = new LinkedHashMap<>(); + String warmup = "?"; + String timed = "?"; + String calls = "?"; + } + + public static void main(String[] args) { + if(args.length < 2) { + System.err.println("Usage: NativeBridgeBenchmarkCompare [output.txt]"); + System.exit(1); + } + + CsvData jniData = readCsv(args[0]); + CsvData ffmData = readCsv(args[1]); + + if(jniData.rows.isEmpty() || ffmData.rows.isEmpty()) { + System.err.println("ERROR: One or both CSV files are empty or could not be read."); + System.exit(1); + } + + String outputPath = args.length >= 3 ? args[2] : null; + printComparison(jniData, ffmData, outputPath); + } + + private static CsvData readCsv(String path) { + CsvData data = new CsvData(); + try(BufferedReader br = new BufferedReader(new FileReader(path))) { + String line; + while((line = br.readLine()) != null) { + line = line.trim(); + if(line.isEmpty()) continue; + // Parse metadata comments (e.g. "# warmup=3") + if(line.startsWith("#")) { + String meta = line.substring(1).trim(); + if(meta.startsWith("warmup=")) data.warmup = meta.substring(7); + else if(meta.startsWith("timed=")) data.timed = meta.substring(6); + else if(meta.startsWith("calls=")) data.calls = meta.substring(6); + continue; + } + // Skip CSV header + if(line.startsWith("label,")) continue; + // Format: label,medianMs,mcallsPerSec + int lastComma = line.lastIndexOf(','); + int secondLastComma = line.lastIndexOf(',', lastComma - 1); + if(secondLastComma < 0) continue; + String label = line.substring(0, secondLastComma); + double medianMs = Double.parseDouble(line.substring(secondLastComma + 1, lastComma)); + double mcalls = Double.parseDouble(line.substring(lastComma + 1)); + data.rows.put(label, new Row(label, medianMs, mcalls)); + } + } catch(IOException e) { + System.err.println("Failed to read CSV: " + path + " — " + e.getMessage()); + } + return data; + } + + private static void printComparison(CsvData jniData, CsvData ffmData, String outputPath) { + Map jniRows = jniData.rows; + Map ffmRows = ffmData.rows; + + // Collect all labels preserving order from JNI file + ArrayList labels = new ArrayList<>(jniRows.keySet()); + for(String l : ffmRows.keySet()) { + if(!labels.contains(l)) labels.add(l); + } + + String sep = "+-" + pad("", 34, '-') + "-+-" + + pad("", 10, '-') + "-+-" + + pad("", 12, '-') + "-+-" + + pad("", 10, '-') + "-+-" + + pad("", 12, '-') + "-+-" + + pad("", 9, '-') + "-+-" + + pad("", 8, '-') + "-+"; + + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("=======================================================================\n"); + sb.append(" JNI vs FFM -- Benchmark Comparison\n"); + sb.append("=======================================================================\n"); + sb.append(" Warm-up rounds : ").append(jniData.warmup).append("\n"); + sb.append(" Timed rounds : ").append(jniData.timed).append("\n"); + sb.append(" Calls/round : ").append(jniData.calls).append("\n"); + sb.append("=======================================================================\n"); + sb.append("\n"); + sb.append(sep).append("\n"); + sb.append(String.format("| %-34s | %10s | %12s | %10s | %12s | %9s | %-8s |%n", + "Benchmark", "JNI (ms)", "JNI Mcalls/s", "FFM (ms)", "FFM Mcalls/s", "Speedup", "Winner")); + sb.append(sep).append("\n"); + + int jniWins = 0; + int ffmWins = 0; + int ties = 0; + + for(String label : labels) { + Row jni = jniRows.get(label); + Row ffm = ffmRows.get(label); + + if(jni == null || ffm == null) { + sb.append(String.format("| %-34s | %10s | %12s | %10s | %12s | %9s | %-8s |%n", + label, + jni != null ? String.format("%.1f", jni.medianMs) : "N/A", + jni != null ? String.format("%.2f", jni.mcallsPerSec) : "N/A", + ffm != null ? String.format("%.1f", ffm.medianMs) : "N/A", + ffm != null ? String.format("%.2f", ffm.mcallsPerSec) : "N/A", + "--", "--")); + continue; + } + + // Speedup: how much faster is the winner (ratio of slower/faster) + double speedup; + String winner; + if(jni.medianMs < ffm.medianMs) { + speedup = ffm.medianMs / jni.medianMs; + winner = "JNI"; + jniWins++; + } else if(ffm.medianMs < jni.medianMs) { + speedup = jni.medianMs / ffm.medianMs; + winner = "FFM"; + ffmWins++; + } else { + speedup = 1.0; + winner = "TIE"; + ties++; + } + + sb.append(String.format("| %-34s | %10.1f | %12.2f | %10.1f | %12.2f | %8.2fx | %-8s |%n", + label, jni.medianMs, jni.mcallsPerSec, + ffm.medianMs, ffm.mcallsPerSec, speedup, winner)); + } + + sb.append(sep).append("\n"); + sb.append("\n"); + sb.append(String.format(" Summary: JNI wins %d, FFM wins %d, Ties %d (out of %d benchmarks)%n", + jniWins, ffmWins, ties, labels.size())); + sb.append("\n"); + + // Print to stdout + String table = sb.toString(); + System.out.print(table); + + // Write to file if output path was provided + if(outputPath != null && !outputPath.isEmpty()) { + try(PrintWriter pw = new PrintWriter(new FileWriter(outputPath))) { + pw.print(table); + System.out.println(" Comparison written to: " + outputPath); + } catch(IOException e) { + System.err.println(" Failed to write comparison file: " + e.getMessage()); + } + } + } + + private static String pad(String s, int width, char c) { + StringBuilder sb = new StringBuilder(s); + while(sb.length() < width) sb.append(c); + return sb.toString(); + } +} + diff --git a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmark.java b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmark.java new file mode 100644 index 00000000..5b10eea6 --- /dev/null +++ b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmark.java @@ -0,0 +1,395 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.Gdx; +import com.github.xpenatan.jParser.example.testlib.TestAttributeClass; +import com.github.xpenatan.jParser.example.testlib.TestBufferManualClass; +import com.github.xpenatan.jParser.example.testlib.TestMethodClass; +import com.github.xpenatan.jParser.example.testlib.TestObjectClass; +import com.github.xpenatan.jparser.idl.helper.IDLIntArray; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; + +/** + * FPS benchmark — measures how native bridge overhead affects frame rate. + *

+ * Each frame executes a fixed number of native calls for the current scenario, + * then returns to let GDX render. A state machine cycles through all scenarios, + * measuring average and minimum FPS for each. + *

+ * This complements {@link NativeBridgeBenchmark} which measures raw throughput + * in a tight loop. The FPS benchmark shows real-world frame rate impact. + */ +public class NativeBridgeFpsBenchmark { + + // --------------------------------------------------------------------------- + // Configuration + // --------------------------------------------------------------------------- + + /** Native calls executed per frame for every scenario. */ + private static final int CALLS_PER_FRAME = 50_000; + /** Seconds to warm up before measuring (lets JIT stabilise). */ + private static final float WARMUP_SECONDS = 2f; + /** Seconds to measure FPS after warm-up. */ + private static final float MEASURE_SECONDS = 5f; + + // --------------------------------------------------------------------------- + // State machine + // --------------------------------------------------------------------------- + + private enum State { IDLE, WARMUP, MEASURE, NEXT, DONE } + + private State state = State.IDLE; + private float elapsed; + private int frameCount; + private float minFrameTime; // worst (longest) frame during measurement + private int scenarioIndex; + + // Reusable native objects + private TestMethodClass methodObj; + private TestObjectClass objectA; + private TestObjectClass objectB; + private TestAttributeClass attrObj; + private TestBufferManualClass bufferObj; + private IDLIntArray intArray; + private ByteBuffer byteBuffer; + + // Scenario definitions + private String[] labels; + private Runnable[] workloads; + + // Collected results + private final ArrayList results = new ArrayList<>(); + + /** Data holder for one scenario measurement. */ + public static class FpsResult { + public final String label; + public final float avgFps; + public final float minFps; + + public FpsResult(String label, float avgFps, float minFps) { + this.label = label; + this.avgFps = avgFps; + this.minFps = minFps; + } + } + + // --------------------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------------------- + + /** Call once after native libraries are loaded. */ + public void start() { + allocateResources(); + buildScenarios(); + results.clear(); + scenarioIndex = 0; + beginScenario(); + + System.out.println("======================================================="); + System.out.println(" FPS Benchmark"); + System.out.println(" Calls/frame : " + CALLS_PER_FRAME); + System.out.println(" Warm-up (sec) : " + (int) WARMUP_SECONDS); + System.out.println(" Measure (sec) : " + (int) MEASURE_SECONDS); + System.out.println(" Scenarios : " + labels.length); + System.out.println("======================================================="); + } + + /** + * Call every frame from {@code render()}. Returns {@code true} when all + * scenarios are finished. + */ + public boolean update() { + if(state == State.DONE) return true; + + float dt = Gdx.graphics.getDeltaTime(); + + // Execute the workload for this frame + if(state == State.WARMUP || state == State.MEASURE) { + workloads[scenarioIndex].run(); + } + + switch(state) { + case WARMUP: + elapsed += dt; + if(elapsed >= WARMUP_SECONDS) { + // Transition to measurement + state = State.MEASURE; + elapsed = 0f; + frameCount = 0; + minFrameTime = 0f; + } + break; + + case MEASURE: + elapsed += dt; + frameCount++; + if(dt > minFrameTime) { + minFrameTime = dt; + } + if(elapsed >= MEASURE_SECONDS) { + // Record results + float avgFps = frameCount / elapsed; + float minFps = minFrameTime > 0 ? 1f / minFrameTime : 0f; + results.add(new FpsResult(labels[scenarioIndex], avgFps, minFps)); + + System.out.printf(" %-38s avg %6.1f FPS min %6.1f FPS%n", + labels[scenarioIndex], avgFps, minFps); + + state = State.NEXT; + } + break; + + case NEXT: + scenarioIndex++; + if(scenarioIndex >= labels.length) { + state = State.DONE; + finish(); + return true; + } + beginScenario(); + break; + + default: + break; + } + + return false; + } + + /** Returns collected results after benchmark is done. */ + public ArrayList getResults() { + return results; + } + + // --------------------------------------------------------------------------- + // Internals + // --------------------------------------------------------------------------- + + private void beginScenario() { + state = State.WARMUP; + elapsed = 0f; + frameCount = 0; + minFrameTime = 0f; + } + + private void finish() { + System.out.println("======================================================="); + System.out.println(" FPS Benchmark complete"); + System.out.println("======================================================="); + + String outputPath = System.getProperty("benchmark.fps.output"); + if(outputPath != null && !outputPath.isEmpty()) { + writeCsv(outputPath); + } + + freeResources(); + } + + private void writeCsv(String path) { + try(PrintWriter pw = new PrintWriter(new FileWriter(path))) { + pw.println("# calls_per_frame=" + CALLS_PER_FRAME); + pw.println("# warmup_sec=" + (int) WARMUP_SECONDS); + pw.println("# measure_sec=" + (int) MEASURE_SECONDS); + pw.println("label,avgFps,minFps"); + for(FpsResult r : results) { + pw.printf("%s,%.2f,%.2f%n", r.label, r.avgFps, r.minFps); + } + System.out.println(" Results written to: " + path); + } catch(IOException e) { + System.err.println(" Failed to write CSV: " + e.getMessage()); + } + } + + // --------------------------------------------------------------------------- + // Resource lifecycle + // --------------------------------------------------------------------------- + + private void allocateResources() { + methodObj = new TestMethodClass(); + objectA = new TestObjectClass(); + objectB = new TestObjectClass(); + attrObj = new TestAttributeClass(); + bufferObj = new TestBufferManualClass(); + intArray = new IDLIntArray(64); + + byteBuffer = ByteBuffer.allocateDirect(256); + byteBuffer.order(ByteOrder.nativeOrder()); + + objectA.set_intValue01(42); + objectA.set_floatValue01(3.14f); + objectB.set_intValue01(99); + objectB.set_floatValue01(2.71f); + methodObj.setMethod05("hello"); + for(int i = 0; i < 64; i++) { + intArray.setValue(i, i); + } + for(int i = 0; i < 256; i++) { + byteBuffer.put(i, (byte) i); + } + } + + private void freeResources() { + intArray.dispose(); + attrObj.dispose(); + objectB.dispose(); + objectA.dispose(); + methodObj.dispose(); + } + + // --------------------------------------------------------------------------- + // Scenario definitions + // --------------------------------------------------------------------------- + + private void buildScenarios() { + labels = new String[] { + "void(int)", + "void(float, bool)", + "void(int,int,float,float,bool)", + "int getter", + "float getter", + "void(String)", + "String getter", + "void(Obj*,Obj*,Obj&,Obj&)", + "Object* getter", + "attribute set int", + "attribute get int", + "attribute set float", + "attribute get float", + "IDLIntArray set+get", + "ByteBuffer update (256B)", + }; + + workloads = new Runnable[] { + this::fpsSetInt, + this::fpsSetFloatBool, + this::fpsSetManyPrimitives, + this::fpsGetInt, + this::fpsGetFloat, + this::fpsSetString, + this::fpsGetString, + this::fpsObjectPassing, + this::fpsObjectReturn, + this::fpsAttrSetInt, + this::fpsAttrGetInt, + this::fpsAttrSetFloat, + this::fpsAttrGetFloat, + this::fpsIDLIntArray, + this::fpsByteBuffer, + }; + } + + // --------------------------------------------------------------------------- + // Per-frame workloads (same calls as throughput benchmark, but N per frame) + // --------------------------------------------------------------------------- + + private void fpsSetInt() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + methodObj.setMethod01(i); + } + } + + private void fpsSetFloatBool() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + methodObj.setMethod02(1.5f, true); + } + } + + private void fpsSetManyPrimitives() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + methodObj.setMethod03(1, 2, 3.0f, 4.0f, true); + } + } + + private void fpsGetInt() { + int sink = 0; + for(int i = 0; i < CALLS_PER_FRAME; i++) { + sink += methodObj.getIntValue01(); + } + intSink = sink; + } + + private void fpsGetFloat() { + float sink = 0; + for(int i = 0; i < CALLS_PER_FRAME; i++) { + sink += methodObj.getFloatValue01(); + } + floatSink = sink; + } + + private void fpsSetString() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + methodObj.setMethod05("benchmark"); + } + } + + private void fpsGetString() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + methodObj.getStrValue01().data(); + } + } + + private void fpsObjectPassing() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + methodObj.setMethod06(objectA, objectB, objectA, objectB); + } + } + + private void fpsObjectReturn() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + methodObj.getPointerObject02(); + } + } + + private void fpsAttrSetInt() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + attrObj.set_intValue01(i); + } + } + + private void fpsAttrGetInt() { + int sink = 0; + for(int i = 0; i < CALLS_PER_FRAME; i++) { + sink += attrObj.get_intValue01(); + } + intSink = sink; + } + + private void fpsAttrSetFloat() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + attrObj.set_floatValue01((float) i); + } + } + + private void fpsAttrGetFloat() { + float sink = 0; + for(int i = 0; i < CALLS_PER_FRAME; i++) { + sink += attrObj.get_floatValue01(); + } + floatSink = sink; + } + + private void fpsIDLIntArray() { + int sink = 0; + for(int i = 0; i < CALLS_PER_FRAME; i++) { + int idx = i & 63; + intArray.setValue(idx, i); + sink += intArray.getValue(idx); + } + intSink = sink; + } + + private void fpsByteBuffer() { + for(int i = 0; i < CALLS_PER_FRAME; i++) { + bufferObj.updateByteBuffer(byteBuffer, 256, (byte) 0x42); + } + } + + // Anti-optimisation fence + private volatile int intSink; + private volatile float floatSink; +} + diff --git a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkApp.java b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkApp.java new file mode 100644 index 00000000..61cb4b1d --- /dev/null +++ b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkApp.java @@ -0,0 +1,51 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.ApplicationAdapter; +import com.badlogic.gdx.Gdx; +import com.badlogic.gdx.graphics.Color; +import com.badlogic.gdx.utils.ScreenUtils; +import com.github.xpenatan.jParser.example.testlib.TestLibLoader; +import com.github.xpenatan.jparser.idl.IDLLoader; + +public class NativeBridgeFpsBenchmarkApp extends ApplicationAdapter { + + private boolean init = false; + private boolean running = false; + private NativeBridgeFpsBenchmark benchmark; + + @Override + public void create() { + IDLLoader.init((idl_isSuccess, idl_e) -> { + if(idl_e != null) { + idl_e.printStackTrace(); + return; + } + TestLibLoader.init((isSuccess, e) -> { + if(e != null) { + e.printStackTrace(); + } + init = isSuccess; + }); + }); + } + + @Override + public void render() { + ScreenUtils.clear(Color.DARK_GRAY); + + if(init && !running) { + init = false; + running = true; + benchmark = new NativeBridgeFpsBenchmark(); + benchmark.start(); + } + + if(running) { + boolean done = benchmark.update(); + if(done) { + Gdx.app.exit(); + } + } + } +} + diff --git a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkCompare.java b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkCompare.java new file mode 100644 index 00000000..db0c6c28 --- /dev/null +++ b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkCompare.java @@ -0,0 +1,184 @@ +package com.github.xpenatan.jParser.example.app; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Reads two FPS benchmark CSV files (JNI and FFM) and prints a side-by-side + * comparison table to stdout, and optionally writes it to a file. + *

+ * Usage: {@code java NativeBridgeFpsBenchmarkCompare [output.txt]} + */ +public class NativeBridgeFpsBenchmarkCompare { + + private static class Row { + final String label; + final double avgFps; + final double minFps; + + Row(String label, double avgFps, double minFps) { + this.label = label; + this.avgFps = avgFps; + this.minFps = minFps; + } + } + + private static class CsvData { + final Map rows = new LinkedHashMap<>(); + String callsPerFrame = "?"; + String warmupSec = "?"; + String measureSec = "?"; + } + + public static void main(String[] args) { + if(args.length < 2) { + System.err.println("Usage: NativeBridgeFpsBenchmarkCompare [output.txt]"); + System.exit(1); + } + + CsvData jniData = readCsv(args[0]); + CsvData ffmData = readCsv(args[1]); + + if(jniData.rows.isEmpty() || ffmData.rows.isEmpty()) { + System.err.println("ERROR: One or both CSV files are empty or could not be read."); + System.exit(1); + } + + String outputPath = args.length >= 3 ? args[2] : null; + printComparison(jniData, ffmData, outputPath); + } + + private static CsvData readCsv(String path) { + CsvData data = new CsvData(); + try(BufferedReader br = new BufferedReader(new FileReader(path))) { + String line; + while((line = br.readLine()) != null) { + line = line.trim(); + if(line.isEmpty()) continue; + if(line.startsWith("#")) { + String meta = line.substring(1).trim(); + if(meta.startsWith("calls_per_frame=")) data.callsPerFrame = meta.substring(16); + else if(meta.startsWith("warmup_sec=")) data.warmupSec = meta.substring(11); + else if(meta.startsWith("measure_sec=")) data.measureSec = meta.substring(12); + continue; + } + if(line.startsWith("label,")) continue; + // Format: label,avgFps,minFps + int lastComma = line.lastIndexOf(','); + int secondLastComma = line.lastIndexOf(',', lastComma - 1); + if(secondLastComma < 0) continue; + String label = line.substring(0, secondLastComma); + double avgFps = Double.parseDouble(line.substring(secondLastComma + 1, lastComma)); + double minFps = Double.parseDouble(line.substring(lastComma + 1)); + data.rows.put(label, new Row(label, avgFps, minFps)); + } + } catch(IOException e) { + System.err.println("Failed to read CSV: " + path + " -- " + e.getMessage()); + } + return data; + } + + private static void printComparison(CsvData jniData, CsvData ffmData, String outputPath) { + Map jniRows = jniData.rows; + Map ffmRows = ffmData.rows; + + ArrayList labels = new ArrayList<>(jniRows.keySet()); + for(String l : ffmRows.keySet()) { + if(!labels.contains(l)) labels.add(l); + } + + String sep = "+-" + pad(34) + "-+-" + + pad(10) + "-+-" + + pad(10) + "-+-" + + pad(10) + "-+-" + + pad(10) + "-+-" + + pad(9) + "-+-" + + pad(8) + "-+"; + + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("=======================================================================\n"); + sb.append(" JNI vs FFM -- FPS Benchmark Comparison\n"); + sb.append("=======================================================================\n"); + sb.append(" Calls/frame : ").append(jniData.callsPerFrame).append("\n"); + sb.append(" Warm-up (sec) : ").append(jniData.warmupSec).append("\n"); + sb.append(" Measure (sec) : ").append(jniData.measureSec).append("\n"); + sb.append("=======================================================================\n"); + sb.append("\n"); + sb.append(sep).append("\n"); + sb.append(String.format("| %-34s | %10s | %10s | %10s | %10s | %9s | %-8s |%n", + "Benchmark", "JNI avg", "JNI min", "FFM avg", "FFM min", "FPS gain", "Winner")); + sb.append(sep).append("\n"); + + int jniWins = 0; + int ffmWins = 0; + int ties = 0; + + for(String label : labels) { + Row jni = jniRows.get(label); + Row ffm = ffmRows.get(label); + + if(jni == null || ffm == null) { + sb.append(String.format("| %-34s | %10s | %10s | %10s | %10s | %9s | %-8s |%n", + label, + jni != null ? String.format("%.1f", jni.avgFps) : "N/A", + jni != null ? String.format("%.1f", jni.minFps) : "N/A", + ffm != null ? String.format("%.1f", ffm.avgFps) : "N/A", + ffm != null ? String.format("%.1f", ffm.minFps) : "N/A", + "--", "--")); + continue; + } + + double gain; + String winner; + if(ffm.avgFps > jni.avgFps) { + gain = ffm.avgFps - jni.avgFps; + winner = "FFM"; + ffmWins++; + } else if(jni.avgFps > ffm.avgFps) { + gain = jni.avgFps - ffm.avgFps; + winner = "JNI"; + jniWins++; + } else { + gain = 0; + winner = "TIE"; + ties++; + } + + sb.append(String.format("| %-34s | %10.1f | %10.1f | %10.1f | %10.1f | %+8.1f | %-8s |%n", + label, jni.avgFps, jni.minFps, + ffm.avgFps, ffm.minFps, winner.equals("FFM") ? gain : -gain, winner)); + } + + sb.append(sep).append("\n"); + sb.append("\n"); + sb.append(String.format(" Summary: JNI wins %d, FFM wins %d, Ties %d (out of %d benchmarks)%n", + jniWins, ffmWins, ties, labels.size())); + sb.append("\n"); + + String table = sb.toString(); + System.out.print(table); + + if(outputPath != null && !outputPath.isEmpty()) { + try(PrintWriter pw = new PrintWriter(new FileWriter(outputPath))) { + pw.print(table); + System.out.println(" Comparison written to: " + outputPath); + } catch(IOException e) { + System.err.println(" Failed to write comparison file: " + e.getMessage()); + } + } + } + + private static String pad(int width) { + StringBuilder sb = new StringBuilder(); + while(sb.length() < width) sb.append('-'); + return sb.toString(); + } +} + diff --git a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/TestLib.java b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/TestLib.java index bc62f4dc..1bfa0bfb 100644 --- a/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/TestLib.java +++ b/examples/TestLib/app/core/src/main/java/com/github/xpenatan/jParser/example/app/TestLib.java @@ -22,8 +22,14 @@ public static boolean test() { ArrayList logs = new ArrayList<>(); boolean allTestsPassed = true; for(CodeTest setupTest : setupTests()) { - boolean result = setupTest.test(); String testName = setupTest.getClass().getSimpleName(); + boolean result; + try { + result = setupTest.test(); + } catch(Throwable e) { + e.printStackTrace(); + result = false; + } logs.add(testName + ": " + result); allTestsPassed = allTestsPassed && result; } diff --git a/examples/TestLib/app/desktop/build.gradle.kts b/examples/TestLib/app/desktop/build.gradle.kts index ecdfd82d..3c40da0e 100644 --- a/examples/TestLib/app/desktop/build.gradle.kts +++ b/examples/TestLib/app/desktop/build.gradle.kts @@ -9,13 +9,26 @@ java { targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) } +// Separate configuration for JNI bridge (used by benchmark comparison tasks only). +val jniBridge by configurations.creating { + isTransitive = true +} + dependencies { implementation(project(":examples:TestLib:app:core")) - implementation(project(":examples:TestLib:lib:lib-core")) + + // Choose ONE native bridge — lib-core (JNI) or lib-ffm (FFM). + // Both provide identical public APIs; only the internal native bridge differs. + // implementation(project(":examples:TestLib:lib:lib-core")) // JNI (default) + implementation(project(":examples:TestLib:lib:lib-ffm")) // FFM (Java 22+, no JNI overhead) + implementation(project(":examples:TestLib:lib:lib-desktop")) implementation("com.badlogicgames.gdx:gdx-platform:${LibExt.gdxVersion}:natives-desktop") implementation("com.badlogicgames.gdx:gdx-backend-lwjgl3:${LibExt.gdxVersion}") + + // JNI bridge for comparison benchmarks (not on default classpath) + jniBridge(project(":examples:TestLib:lib:lib-core")) } tasks.register("TestLib_run_app_desktop") { @@ -24,6 +37,12 @@ tasks.register("TestLib_run_app_desktop") { mainClass.set("com.github.xpenatan.jParser.example.app.Main") classpath = sourceSets["main"].runtimeClasspath + // FFM requires JDK 22+ and native access + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + jvmArgs("--enable-native-access=ALL-UNNAMED") + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { jvmArgs("-XstartOnFirstThread") } @@ -38,4 +57,154 @@ tasks.register("TestLib_run_benchmark_desktop") { if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { jvmArgs("-XstartOnFirstThread") } -} \ No newline at end of file +} + +tasks.register("TestLib_run_native_benchmark_desktop") { + group = "example-desktop" + description = "Run native bridge benchmark (JNI vs FFM)" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkMain") + classpath = sourceSets["main"].runtimeClasspath + + // FFM requires JDK 22+ and native access + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + jvmArgs("--enable-native-access=ALL-UNNAMED") + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } +} + +// --------------------------------------------------------------------------- +// JNI vs FFM comparison benchmark tasks +// --------------------------------------------------------------------------- + +// Build a JNI classpath: take the default runtime classpath, remove lib-ffm +// artifacts, and add lib-core (JNI) from the jniBridge configuration. +// The JNI native DLL directories are prepended so the JNI-compiled DLLs are +// found by the loader *before* the FFM DLLs bundled inside the desktop JARs. +val testLibJniDllDir = file("${projectDir}/../../lib/lib-build/build/c++/libs/windows/vc") +val idlHelperJniDllDir = file("${rootProject.projectDir}/idl-helper/idl-helper-build/build/c++/libs/windows/vc") +val jniClasspath = files(testLibJniDllDir, idlHelperJniDllDir) + sourceSets["main"].runtimeClasspath.filter { file -> + !file.absolutePath.replace('\\', '/').contains("lib-ffm") && + !file.absolutePath.replace('\\', '/').contains("idl-helper-ffm") +} + configurations["jniBridge"] + +val benchmarkDir = layout.buildDirectory.dir("benchmark") + +tasks.register("TestLib_benchmark_jni") { + group = "example-benchmark" + description = "Run native bridge benchmark with JNI bridge, save CSV report" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkMain") + classpath = jniClasspath + systemProperty("benchmark.output", benchmarkDir.get().file("benchmark_jni.csv").asFile.absolutePath) + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } + + doFirst { + benchmarkDir.get().asFile.mkdirs() + } +} + +tasks.register("TestLib_benchmark_ffm") { + group = "example-benchmark" + description = "Run native bridge benchmark with FFM bridge, save CSV report" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkMain") + classpath = sourceSets["main"].runtimeClasspath + systemProperty("benchmark.output", benchmarkDir.get().file("benchmark_ffm.csv").asFile.absolutePath) + + // FFM requires JDK 22+ and native access + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + jvmArgs("--enable-native-access=ALL-UNNAMED") + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } + + doFirst { + benchmarkDir.get().asFile.mkdirs() + } +} + +tasks.register("TestLib_benchmark_compare") { + group = "example-benchmark" + description = "Run JNI & FFM benchmarks then print a comparison table" + dependsOn("TestLib_benchmark_jni", "TestLib_benchmark_ffm") + + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkCompare") + classpath = sourceSets["main"].runtimeClasspath + args( + benchmarkDir.get().file("benchmark_jni.csv").asFile.absolutePath, + benchmarkDir.get().file("benchmark_ffm.csv").asFile.absolutePath, + benchmarkDir.get().file("benchmark_compare.txt").asFile.absolutePath + ) +} + +// Ensure JNI runs before FFM when both are requested +tasks.named("TestLib_benchmark_ffm") { mustRunAfter("TestLib_benchmark_jni") } + +// --------------------------------------------------------------------------- +// JNI vs FFM FPS benchmark tasks +// --------------------------------------------------------------------------- + +val fpsBenchmarkDir = layout.buildDirectory.dir("benchmark") + +tasks.register("TestLib_fps_benchmark_jni") { + group = "example-benchmark" + description = "Run FPS benchmark with JNI bridge, save CSV report" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkMain") + classpath = jniClasspath + systemProperty("benchmark.fps.output", fpsBenchmarkDir.get().file("fps_benchmark_jni.csv").asFile.absolutePath) + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } + + doFirst { + fpsBenchmarkDir.get().asFile.mkdirs() + } +} + +tasks.register("TestLib_fps_benchmark_ffm") { + group = "example-benchmark" + description = "Run FPS benchmark with FFM bridge, save CSV report" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkMain") + classpath = sourceSets["main"].runtimeClasspath + systemProperty("benchmark.fps.output", fpsBenchmarkDir.get().file("fps_benchmark_ffm.csv").asFile.absolutePath) + + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + jvmArgs("--enable-native-access=ALL-UNNAMED") + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } + + doFirst { + fpsBenchmarkDir.get().asFile.mkdirs() + } +} + +tasks.register("TestLib_fps_benchmark_compare") { + group = "example-benchmark" + description = "Run JNI & FFM FPS benchmarks then print a comparison table" + dependsOn("TestLib_fps_benchmark_jni", "TestLib_fps_benchmark_ffm") + + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkCompare") + classpath = sourceSets["main"].runtimeClasspath + args( + fpsBenchmarkDir.get().file("fps_benchmark_jni.csv").asFile.absolutePath, + fpsBenchmarkDir.get().file("fps_benchmark_ffm.csv").asFile.absolutePath, + fpsBenchmarkDir.get().file("fps_benchmark_compare.txt").asFile.absolutePath + ) +} + +// Ensure JNI runs before FFM when both are requested +tasks.named("TestLib_fps_benchmark_ffm") { mustRunAfter("TestLib_fps_benchmark_jni") } + diff --git a/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java b/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java new file mode 100644 index 00000000..d28d0cc1 --- /dev/null +++ b/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java @@ -0,0 +1,14 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; + +public class NativeBridgeBenchmarkMain { + + public static void main(String[] args) { + Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration(); + config.setTitle("Native Bridge Benchmark"); + new Lwjgl3Application(new NativeBridgeBenchmarkApp(), config); + } +} + diff --git a/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java b/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java new file mode 100644 index 00000000..00051aaa --- /dev/null +++ b/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java @@ -0,0 +1,16 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; + +public class NativeBridgeFpsBenchmarkMain { + + public static void main(String[] args) { + Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration(); + config.setTitle("FPS Benchmark"); + config.useVsync(false); + config.setForegroundFPS(0); + new Lwjgl3Application(new NativeBridgeFpsBenchmarkApp(), config); + } +} + diff --git a/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/CallbackClassManual.java b/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/CallbackClassManual.java index 8afd6726..8080ef78 100644 --- a/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/CallbackClassManual.java +++ b/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/CallbackClassManual.java @@ -55,6 +55,81 @@ virtual void onStringCallback(const char* strValue01) const { }; */ + /*[-FFM;-NATIVE] + typedef void (*fp_CCMImpl_onVoidCallback)(int64_t, int64_t); + typedef int32_t (*fp_CCMImpl_onIntCallback)(int32_t, int32_t); + typedef float (*fp_CCMImpl_onFloatCallback)(float, float); + typedef int32_t (*fp_CCMImpl_onBoolCallback)(int32_t); + typedef void (*fp_CCMImpl_onStringCallback)(const char*); + class CallbackClassManualImpl : public CallbackClassManual { + private: + fp_CCMImpl_onVoidCallback onVoidCallback_ptr; + fp_CCMImpl_onIntCallback onIntCallback_ptr; + fp_CCMImpl_onFloatCallback onFloatCallback_ptr; + fp_CCMImpl_onBoolCallback onBoolCallback_ptr; + fp_CCMImpl_onStringCallback onStringCallback_ptr; + public: + void setupCallback(fp_CCMImpl_onVoidCallback a, fp_CCMImpl_onIntCallback b, fp_CCMImpl_onFloatCallback c, fp_CCMImpl_onBoolCallback d, fp_CCMImpl_onStringCallback e) { + this->onVoidCallback_ptr = a; + this->onIntCallback_ptr = b; + this->onFloatCallback_ptr = c; + this->onBoolCallback_ptr = d; + this->onStringCallback_ptr = e; + } + virtual void onVoidCallback(TestObjectClass& refData, TestObjectClass* pointerData) const { + onVoidCallback_ptr((int64_t)&refData, (int64_t)pointerData); + } + virtual int onIntCallback(int intValue01, int intValue02) const { + return (int)onIntCallback_ptr(intValue01, intValue02); + } + virtual float onFloatCallback(float floatValue01, float floatValue02) const { + return (float)onFloatCallback_ptr(floatValue01, floatValue02); + } + virtual bool onBoolCallback(bool boolValue01) const { + return (bool)onBoolCallback_ptr(boolValue01); + } + virtual void onStringCallback(const char* strValue01) const { + onStringCallback_ptr(strValue01); + } + }; + + extern "C" { + FFM_EXPORT int64_t jparser_com_github_xpenatan_jParser_example_testlib_CallbackClassManual_internal_1native_1create_1addr__(void) { + return (int64_t)new CallbackClassManualImpl(); + } + FFM_EXPORT int64_t jparser_com_github_xpenatan_jParser_example_testlib_CallbackClassManual_internal_1getAndroidCode__(void) { + long long myCode = 0; + myCode++; + #ifdef __ANDROID__ + return 1; + #else + return 0; + #endif + } + FFM_EXPORT void jparser_com_github_xpenatan_jParser_example_testlib_CallbackClassManual_internal_1native_1setupCallbacks__JJJJJJ(int64_t this_addr, int64_t onVoidCallback_fp, int64_t onIntCallback_fp, int64_t onFloatCallback_fp, int64_t onBoolCallback_fp, int64_t onStringCallback_fp) { + CallbackClassManualImpl* nativeObject = (CallbackClassManualImpl*)this_addr; + nativeObject->setupCallback((fp_CCMImpl_onVoidCallback)onVoidCallback_fp, (fp_CCMImpl_onIntCallback)onIntCallback_fp, (fp_CCMImpl_onFloatCallback)onFloatCallback_fp, (fp_CCMImpl_onBoolCallback)onBoolCallback_fp, (fp_CCMImpl_onStringCallback)onStringCallback_fp); + } + } + */ + + /*[-FFM;-ADD] + private void internal_ffm_onStringCallback(java.lang.foreign.MemorySegment seg) { + String str = seg.reinterpret(Long.MAX_VALUE).getString(0); + internal_onStringCallback(str); + } + */ + + /*[-FFM;-ADD] + private static final class FFMHandles { + private static final java.lang.foreign.SymbolLookup LOOKUP = java.lang.foreign.SymbolLookup.loaderLookup(); + private static final java.lang.foreign.Linker LINKER = java.lang.foreign.Linker.nativeLinker(); + static final java.lang.invoke.MethodHandle create_addr = LINKER.downcallHandle(LOOKUP.find("jparser_com_github_xpenatan_jParser_example_testlib_CallbackClassManual_internal_1native_1create_1addr__").orElseThrow(), java.lang.foreign.FunctionDescriptor.of(java.lang.foreign.ValueLayout.JAVA_LONG)); + static final java.lang.invoke.MethodHandle getAndroidCode = LINKER.downcallHandle(LOOKUP.find("jparser_com_github_xpenatan_jParser_example_testlib_CallbackClassManual_internal_1getAndroidCode__").orElseThrow(), java.lang.foreign.FunctionDescriptor.of(java.lang.foreign.ValueLayout.JAVA_LONG)); + static final java.lang.invoke.MethodHandle setupCallbacks = LINKER.downcallHandle(LOOKUP.find("jparser_com_github_xpenatan_jParser_example_testlib_CallbackClassManual_internal_1native_1setupCallbacks__JJJJJJ").orElseThrow(), java.lang.foreign.FunctionDescriptor.ofVoid(java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG)); + } + */ + /*[-TEAVM;-ADD] @org.teavm.jso.JSFunctor public interface onVoidCallback extends org.teavm.jso.JSObject { @@ -108,6 +183,12 @@ public static long GetAndroidCode() { return 0; #endif */ + /*[-FFM;-REPLACE] + private static long internal_getAndroidCode() { + try { return (long) FFMHandles.getAndroidCode.invokeExact(); } + catch(Throwable e) { throw new RuntimeException(e); } + } + */ private static native long internal_getAndroidCode(); /*[-JNI;-NATIVE] @@ -117,8 +198,33 @@ public static long GetAndroidCode() { var CallbackClassManualImpl = new [MODULE].CallbackClassManualImpl(); return [MODULE].getPointer(CallbackClassManualImpl); */ + /*[-FFM;-REPLACE] + private static long internal_native_create_addr() { + try { return (long) FFMHandles.create_addr.invokeExact(); } + catch(Throwable e) { throw new RuntimeException(e); } + } + */ private static native long internal_native_create_addr(); + /*[-FFM;-REPLACE_BLOCK] + { + try { + java.lang.invoke.MethodHandle mh_void = java.lang.invoke.MethodHandles.lookup().findVirtual(CallbackClassManual.class, "internal_onVoidCallback", java.lang.invoke.MethodType.methodType(void.class, long.class, long.class)).bindTo(this); + java.lang.foreign.MemorySegment stub_void = java.lang.foreign.Linker.nativeLinker().upcallStub(mh_void, java.lang.foreign.FunctionDescriptor.ofVoid(java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG), java.lang.foreign.Arena.ofAuto()); + java.lang.invoke.MethodHandle mh_int = java.lang.invoke.MethodHandles.lookup().findVirtual(CallbackClassManual.class, "internal_onIntCallback", java.lang.invoke.MethodType.methodType(int.class, int.class, int.class)).bindTo(this); + java.lang.foreign.MemorySegment stub_int = java.lang.foreign.Linker.nativeLinker().upcallStub(mh_int, java.lang.foreign.FunctionDescriptor.of(java.lang.foreign.ValueLayout.JAVA_INT, java.lang.foreign.ValueLayout.JAVA_INT, java.lang.foreign.ValueLayout.JAVA_INT), java.lang.foreign.Arena.ofAuto()); + java.lang.invoke.MethodHandle mh_float = java.lang.invoke.MethodHandles.lookup().findVirtual(CallbackClassManual.class, "internal_onFloatCallback", java.lang.invoke.MethodType.methodType(float.class, float.class, float.class)).bindTo(this); + java.lang.foreign.MemorySegment stub_float = java.lang.foreign.Linker.nativeLinker().upcallStub(mh_float, java.lang.foreign.FunctionDescriptor.of(java.lang.foreign.ValueLayout.JAVA_FLOAT, java.lang.foreign.ValueLayout.JAVA_FLOAT, java.lang.foreign.ValueLayout.JAVA_FLOAT), java.lang.foreign.Arena.ofAuto()); + java.lang.invoke.MethodHandle mh_bool = java.lang.invoke.MethodHandles.lookup().findVirtual(CallbackClassManual.class, "internal_onBoolCallback", java.lang.invoke.MethodType.methodType(boolean.class, boolean.class)).bindTo(this); + java.lang.foreign.MemorySegment stub_bool = java.lang.foreign.Linker.nativeLinker().upcallStub(mh_bool, java.lang.foreign.FunctionDescriptor.of(java.lang.foreign.ValueLayout.JAVA_BOOLEAN, java.lang.foreign.ValueLayout.JAVA_BOOLEAN), java.lang.foreign.Arena.ofAuto()); + java.lang.invoke.MethodHandle mh_string = java.lang.invoke.MethodHandles.lookup().findVirtual(CallbackClassManual.class, "internal_ffm_onStringCallback", java.lang.invoke.MethodType.methodType(void.class, java.lang.foreign.MemorySegment.class)).bindTo(this); + java.lang.foreign.MemorySegment stub_string = java.lang.foreign.Linker.nativeLinker().upcallStub(mh_string, java.lang.foreign.FunctionDescriptor.ofVoid(java.lang.foreign.ValueLayout.ADDRESS), java.lang.foreign.Arena.ofAuto()); + internal_native_setupCallbacks(native_address, stub_void.address(), stub_int.address(), stub_float.address(), stub_bool.address(), stub_string.address()); + } catch(Throwable e) { + throw new RuntimeException(e); + } + } + */ /*[-TEAVM;-REPLACE_BLOCK] { onVoidCallback onVoidCallback = new onVoidCallback() { @@ -166,6 +272,12 @@ private void setupCallbacks() { @org.teavm.jso.JSBody(params = { "this_addr", "onVoidCallback", "onIntCallback", "onFloatCallback", "onBoolCallback", "onStringCallback" }, script = "var CallbackClassManualImpl = [MODULE].wrapPointer(this_addr, [MODULE].CallbackClassManualImpl); CallbackClassManualImpl.onVoidCallback = onVoidCallback; CallbackClassManualImpl.onIntCallback = onIntCallback; CallbackClassManualImpl.onFloatCallback = onFloatCallback; CallbackClassManualImpl.onBoolCallback = onBoolCallback; CallbackClassManualImpl.onStringCallback = onStringCallback;") private static native void internal_native_setupCallbacks(int this_addr, onVoidCallback onVoidCallback, onIntCallback onIntCallback, onFloatCallback onFloatCallback, onBoolCallback onBoolCallback, onStringCallback onStringCallback); */ + /*[-FFM;-REPLACE] + private static void internal_native_setupCallbacks(long this_addr, long onVoidCallback_fp, long onIntCallback_fp, long onFloatCallback_fp, long onBoolCallback_fp, long onStringCallback_fp) { + try { FFMHandles.setupCallbacks.invokeExact(this_addr, onVoidCallback_fp, onIntCallback_fp, onFloatCallback_fp, onBoolCallback_fp, onStringCallback_fp); } + catch(Throwable e) { throw new RuntimeException(e); } + } + */ private native void internal_native_setupCallbacks(long this_addr); public void internal_onVoidCallback(long refData, long pointerData) { diff --git a/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestBufferManualClass.java b/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestBufferManualClass.java index adf0e2fd..eb0f0b02 100644 --- a/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestBufferManualClass.java +++ b/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestBufferManualClass.java @@ -5,12 +5,36 @@ public class TestBufferManualClass extends IDLBase { + /*[-FFM;-NATIVE] + extern "C" { + FFM_EXPORT void jparser_com_github_xpenatan_jParser_example_testlib_TestBufferManualClass_internal_1native_1updateByteBuffer__JJIB(int64_t this_addr, int64_t data_ptr, int32_t size, int8_t value) { + TestBufferManualClass* nativeObject = (TestBufferManualClass*)this_addr; + unsigned char* byteData = (unsigned char*)data_ptr; + nativeObject->updateByteBuffer(byteData, (int)size, (unsigned char)value); + } + } + */ + + /*[-FFM;-ADD] + private static final class FFMHandles { + private static final java.lang.foreign.SymbolLookup LOOKUP = java.lang.foreign.SymbolLookup.loaderLookup(); + private static final java.lang.foreign.Linker LINKER = java.lang.foreign.Linker.nativeLinker(); + static final java.lang.invoke.MethodHandle updateByteBuffer = LINKER.downcallHandle(LOOKUP.find("jparser_com_github_xpenatan_jParser_example_testlib_TestBufferManualClass_internal_1native_1updateByteBuffer__JJIB").orElseThrow(), java.lang.foreign.FunctionDescriptor.ofVoid(java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_INT, java.lang.foreign.ValueLayout.JAVA_BYTE)); + } + */ + /*[-TEAVM;-REPLACE_BLOCK] { org.teavm.jso.typedarrays.Uint8Array array = org.teavm.jso.typedarrays.Uint8Array.fromJavaBuffer(data); internal_native_updateByteBuffer(native_address, array, size, value); } */ + /*[-FFM;-REPLACE_BLOCK] + { + java.lang.foreign.MemorySegment seg = java.lang.foreign.MemorySegment.ofBuffer(data); + internal_native_updateByteBuffer(native_address, seg.address(), size, value); + } + */ public void updateByteBuffer(ByteBuffer data, int size, byte value) { internal_native_updateByteBuffer(native_address, data, size, value); } @@ -25,5 +49,11 @@ public void updateByteBuffer(ByteBuffer data, int size, byte value) { unsigned char* byteData = static_cast(dataAddress); nativeObject->updateByteBuffer(byteData, (int)size, (unsigned char)value); */ + /*[-FFM;-REPLACE] + private static void internal_native_updateByteBuffer(long this_addr, long data_ptr, int size, byte value) { + try { FFMHandles.updateByteBuffer.invokeExact(this_addr, data_ptr, size, value); } + catch(Throwable e) { throw new RuntimeException(e); } + } + */ private static native void internal_native_updateByteBuffer(long this_addr, ByteBuffer data, int size, byte value); } \ No newline at end of file diff --git a/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestLibLoader.java b/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestLibLoader.java index 15ec63a6..57e692ea 100644 --- a/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestLibLoader.java +++ b/examples/TestLib/lib/lib-base/src/main/java/com/github/xpenatan/jParser/example/testlib/TestLibLoader.java @@ -11,6 +11,10 @@ public class TestLibLoader { #include "CustomCode.h" */ + /*[-FFM;-NATIVE] + #include "CustomCode.h" + */ + public static void init(JParserLibraryLoaderListener listener) { JParserLibraryLoader.load(LIB_NAME, listener); } diff --git a/examples/TestLib/lib/lib-build/src/main/cpp/source/TestLib/src/TestLib.h b/examples/TestLib/lib/lib-build/src/main/cpp/source/TestLib/src/TestLib.h index 8325de3e..dedbd32d 100644 --- a/examples/TestLib/lib/lib-build/src/main/cpp/source/TestLib/src/TestLib.h +++ b/examples/TestLib/lib/lib-build/src/main/cpp/source/TestLib/src/TestLib.h @@ -165,36 +165,24 @@ class TestBufferManualClass { public: void updateByteBuffer(unsigned char* data, int size, unsigned char value) { - for(int i = 0; i < size; i++) { - std::cout << "[" << i << "]: " << static_cast(data[i]) << std::endl; - } for(int i = 0; i < size; i++) { data[i] = value; } } void updateIntBuffer(int* data, int size, int value) { - for(int i = 0; i < size; i++) { - std::cout << "[" << i << "]: " << data[i] << std::endl; - } for(int i = 0; i < size; i++) { data[i] = value; } } void updateShortBuffer(short* data, int size, short value) { - for(int i = 0; i < size; i++) { - std::cout << "[" << i << "]: " << data[i] << std::endl; - } for(int i = 0; i < size; i++) { data[i] = value; } } void updateFloatBuffer(float* data, int size, float value) { - for(int i = 0; i < size; i++) { - std::cout << "[" << i << "]: " << data[i] << std::endl; - } for(int i = 0; i < size; i++) { data[i] = value; } diff --git a/examples/TestLib/lib/lib-desktop/build.gradle.kts b/examples/TestLib/lib/lib-desktop/build.gradle.kts index 04d89149..83946952 100644 --- a/examples/TestLib/lib/lib-desktop/build.gradle.kts +++ b/examples/TestLib/lib/lib-desktop/build.gradle.kts @@ -9,7 +9,7 @@ java { val libDir = "${projectDir}/../lib-build/build/c++/libs" //val windowsFile = "$libDir/windows/TestLib64.dll" -val windowsFile = "$libDir/windows/vc/TestLib64.dll" +val windowsFile = "$libDir/windows/vc/ffm/TestLib64.dll" val linuxFile = "$libDir/linux/libTestLib64.so" val macFile = "$libDir/mac/libTestLib64.dylib" val macArmFile = "$libDir/mac/arm/libTestLibarm64.dylib" diff --git a/examples/TestLib/lib/lib-ffm/build.gradle.kts b/examples/TestLib/lib/lib-ffm/build.gradle.kts index f9c93be5..91ec2327 100644 --- a/examples/TestLib/lib/lib-ffm/build.gradle.kts +++ b/examples/TestLib/lib/lib-ffm/build.gradle.kts @@ -3,23 +3,36 @@ plugins { id("java-library") } -// NOTE: Generated FFM Java code requires Java 24+ to compile (uses java.lang.foreign.*). -// Set to Java 11 as a placeholder so Gradle can configure the module even without Java 24 installed. +// FFM (java.lang.foreign.*) requires JDK 22+ at runtime. +// We compile with JDK 25 to access the FFM API, but set targetCompatibility +// to Java 11 so Gradle's dependency metadata stays compatible with lower-JVM +// consumer modules. The --release flag is cleared so the JDK 25 API is +// available despite the Java 11 bytecode target. +// It is the consumer's responsibility to run the application on JDK 22+. java { - sourceCompatibility = JavaVersion.toVersion(LibExt.java11Target) - targetCompatibility = JavaVersion.toVersion(LibExt.java11Target) + sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) +} + +tasks.withType { + javaCompiler.set(javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + // Clear --release so JDK 25 APIs (java.lang.foreign) are accessible + // even with -source 11 -target 11 bytecode output. + options.release.set(null as Int?) } dependencies { if(LibExt.exampleUseRepoLibs) { api("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") api("com.github.xpenatan.jParser:idl-core:-SNAPSHOT") - api("com.github.xpenatan.jParser:idl-helper-core:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-helper-ffm:-SNAPSHOT") } else { api(project(":loader:loader-core")) api(project(":idl:idl-core")) - api(project(":idl-helper:idl-helper-core")) + api(project(":idl-helper:idl-helper-ffm")) } } diff --git a/idl-helper/idl-helper-base/src/main/java/idl/IDLLoader.java b/idl-helper/idl-helper-base/src/main/java/idl/IDLLoader.java index 8099c21a..df70fea7 100644 --- a/idl-helper/idl-helper-base/src/main/java/idl/IDLLoader.java +++ b/idl-helper/idl-helper-base/src/main/java/idl/IDLLoader.java @@ -11,6 +11,10 @@ public class IDLLoader { #include "IDLCustomCode.h" */ + /*[-FFM;-NATIVE] + #include "IDLCustomCode.h" + */ + public static void init(JParserLibraryLoaderListener listener) { JParserLibraryLoader.load(LIB_NAME, listener); } diff --git a/idl-helper/idl-helper-build/build.gradle.kts b/idl-helper/idl-helper-build/build.gradle.kts index 4c4a0349..b86d2e3f 100644 --- a/idl-helper/idl-helper-build/build.gradle.kts +++ b/idl-helper/idl-helper-build/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(project(":jParser:jParser-idl")) implementation(project(":jParser:jParser-teavm")) implementation(project(":jParser:jParser-cpp")) + implementation(project(":jParser:jParser-ffm")) implementation(project(":jParser:jParser-build")) implementation(project(":jParser:jParser-build-tool")) } @@ -89,4 +90,45 @@ tasks.register("idl_helper_build_project_ios") { mainClass.set(mainClassName) args = mutableListOf("ios") classpath = sourceSets["main"].runtimeClasspath -} \ No newline at end of file +} + +// FFM tasks — generate FFM Java code and optionally compile FFM native libs +tasks.register("idl_helper_build_project_ffm") { + group = "idl-helper" + description = "Generate FFM code for idl-helper" + mainClass.set(mainClassName) + args = mutableListOf("ffm") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("idl_helper_build_project_ffm_windows64") { + group = "idl-helper" + description = "Generate FFM code + compile FFM native for Windows" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_windows64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("idl_helper_build_project_ffm_linux64") { + group = "idl-helper" + description = "Generate FFM code + compile FFM native for Linux" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_linux64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("idl_helper_build_project_ffm_mac64") { + group = "idl-helper" + description = "Generate FFM code + compile FFM native for macOS" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_mac64") + classpath = sourceSets["main"].runtimeClasspath +} + +tasks.register("idl_helper_build_project_ffm_macArm") { + group = "idl-helper" + description = "Generate FFM code + compile FFM native for macOS ARM" + mainClass.set(mainClassName) + args = mutableListOf("ffm", "ffm_macArm") + classpath = sourceSets["main"].runtimeClasspath +} diff --git a/idl-helper/idl-helper-build/src/main/java/BuildIDLHelper.java b/idl-helper/idl-helper-build/src/main/java/BuildIDLHelper.java index 7964e4ad..1e476deb 100644 --- a/idl-helper/idl-helper-build/src/main/java/BuildIDLHelper.java +++ b/idl-helper/idl-helper-build/src/main/java/BuildIDLHelper.java @@ -31,6 +31,11 @@ public static void main(String[] args) throws Exception { data.modulePrefix = modulePrefix; BuildToolOptions op = new BuildToolOptions(data, args); + // Enable FFM code generation if requested + if(op.containsArg("ffm")) { + op.generateFFM = true; + } + BuilderTool.build(op, new BuildToolListener() { @Override public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList targets) { @@ -55,6 +60,20 @@ public void onAddTarget(BuildToolOptions op, IDLReader idlReader, ArrayList/ffm/ to avoid conflicts with JNI libs. + + private static BuildMultiTarget getFFMWindowVCTarget(BuildToolOptions op) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + WindowsMSVCTarget compileStaticTarget = new WindowsMSVCTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "windows/vc/ffm"; + compileStaticTarget.cppFlags.add("-std:c++11"); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(libBuildCPPPath + "/src/idl/IDLHelper.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + WindowsMSVCTarget linkTarget = new WindowsMSVCTarget(); + linkTarget.libDirSuffix = "windows/vc/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std:c++11"); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("/WHOLEARCHIVE:" + libBuildCPPPath + "/libs/windows/vc/ffm/" + op.libName + "64_.lib"); + linkTarget.linkerFlags.add("-DLL"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMLinuxTarget(BuildToolOptions op) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + LinuxTarget compileStaticTarget = new LinuxTarget(); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = "linux/ffm"; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(libBuildCPPPath + "/src/idl/IDLHelper.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + LinuxTarget linkTarget = new LinuxTarget(); + linkTarget.libDirSuffix = "linux/ffm"; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("-Wl,--whole-archive"); + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/linux/ffm/lib" + op.libName + "64_.a"); + linkTarget.linkerFlags.add("-Wl,--no-whole-archive"); + multiTarget.add(linkTarget); + + return multiTarget; + } + + private static BuildMultiTarget getFFMMacTarget(BuildToolOptions op, boolean isArm) { + BuildMultiTarget multiTarget = new BuildMultiTarget(); + String libBuildCPPPath = op.getModuleBuildCPPPath(); + + String macSubDir = isArm ? "mac/arm/ffm" : "mac/ffm"; + + MacTarget compileStaticTarget = new MacTarget(isArm); + compileStaticTarget.isStatic = true; + compileStaticTarget.libDirSuffix = macSubDir; + compileStaticTarget.cppFlags.add("-std=c++11"); + compileStaticTarget.cppFlags.add("-fPIC"); + compileStaticTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + compileStaticTarget.cppInclude.add(libBuildCPPPath + "/src/idl/IDLHelper.cpp"); + compileStaticTarget.cppInclude.add(op.getCustomSourceDir() + "*.cpp"); + multiTarget.add(compileStaticTarget); + + MacTarget linkTarget = new MacTarget(isArm); + linkTarget.libDirSuffix = macSubDir; + linkTarget.addFFMGlueCode(libBuildCPPPath); + linkTarget.cppFlags.add("-std=c++11"); + linkTarget.cppFlags.add("-fPIC"); + linkTarget.headerDirs.add("-I" + op.getCustomSourceDir()); + linkTarget.linkerFlags.add("-Wl,-force_load"); + if(isArm) { + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/arm/ffm/lib" + op.libName + "64_.a"); + } + else { + linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/mac/ffm/lib" + op.libName + "64_.a"); + } + multiTarget.add(linkTarget); + + return multiTarget; + } } \ No newline at end of file diff --git a/idl-helper/idl-helper-desktop/build.gradle.kts b/idl-helper/idl-helper-desktop/build.gradle.kts index deaec68f..7d62183c 100644 --- a/idl-helper/idl-helper-desktop/build.gradle.kts +++ b/idl-helper/idl-helper-desktop/build.gradle.kts @@ -10,7 +10,7 @@ java { } val libDir = "${projectDir}/../idl-helper-build/build/c++/libs" -val windowsFile = "$libDir/windows/vc/idl64.dll" +val windowsFile = "$libDir/windows/vc/ffm/idl64.dll" val linuxFile = "$libDir/linux/libidl64.so" val macFile = "$libDir/mac/libidl64.dylib" val macArmFile = "$libDir/mac/arm/libidlarm64.dylib" diff --git a/idl-helper/idl-helper-ffm/build.gradle.kts b/idl-helper/idl-helper-ffm/build.gradle.kts new file mode 100644 index 00000000..acbac62a --- /dev/null +++ b/idl-helper/idl-helper-ffm/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("java") +} + +val moduleName = "idl-helper-ffm" + +dependencies { + implementation(project(":idl:idl-core")) + implementation(project(":loader:loader-core")) +} + +// FFM (java.lang.foreign.*) requires JDK 22+ at runtime. +// Compile with JDK 25 for API access, but target Java 8 bytecode +// so Gradle metadata stays compatible with lower-JVM consumers. +java { + sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) +} + +tasks.withType { + javaCompiler.set(javaToolchains.compilerFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + options.release.set(null as Int?) +} + +java { + withJavadocJar() + withSourcesJar() +} + +tasks.named("clean") { + doFirst { + val srcPath = "$projectDir/src/main/java" + project.delete(files(srcPath)) + } +} + +publishing { + publications { + create("maven") { + artifactId = moduleName + group = LibExt.groupId + version = LibExt.libVersion + from(components["java"]) + } + } +} + diff --git a/jParser/jParser-base/src/main/java/idl/helper/IDLArray.java b/jParser/jParser-base/src/main/java/idl/helper/IDLArray.java index df38a3e3..e13cd9cc 100644 --- a/jParser/jParser-base/src/main/java/idl/helper/IDLArray.java +++ b/jParser/jParser-base/src/main/java/idl/helper/IDLArray.java @@ -39,5 +39,9 @@ public void resize(int size) { IDL::IDLArray* nativeObject = (IDL::IDLArray*)this_addr; nativeObject->resize(size); */ + /*[-FFM;-NATIVE] + IDL::IDLArray* nativeObject = (IDL::IDLArray*)this_addr; + nativeObject->resize(size); + */ public static native void internal_native_resize(long this_addr, int size); } diff --git a/jParser/jParser-base/src/main/java/idl/helper/IDLString.java b/jParser/jParser-base/src/main/java/idl/helper/IDLString.java index 477a8abb..6c0ea13e 100644 --- a/jParser/jParser-base/src/main/java/idl/helper/IDLString.java +++ b/jParser/jParser-base/src/main/java/idl/helper/IDLString.java @@ -34,6 +34,10 @@ public String c_str() { var returnedJSObj = jsObj.c_str(); return returnedJSObj; */ + /*[-FFM;-NATIVE] + IDLString* nativeObject = (IDLString*)this_addr; + return nativeObject->c_str(); + */ private static native String internal_native_c_str(long this_addr); public String data() { @@ -52,5 +56,9 @@ public String data() { var returnedJSObj = jsObj.data(); return returnedJSObj; */ + /*[-FFM;-NATIVE] + IDLString* nativeObject = (IDLString*)this_addr; + return nativeObject->data(); + */ private static native String internal_native_data(long this_addr); } \ No newline at end of file diff --git a/jParser/jParser-base/src/main/java/idl/helper/IDLUtils.java b/jParser/jParser-base/src/main/java/idl/helper/IDLUtils.java index 64bad66f..28e73822 100644 --- a/jParser/jParser-base/src/main/java/idl/helper/IDLUtils.java +++ b/jParser/jParser-base/src/main/java/idl/helper/IDLUtils.java @@ -23,6 +23,12 @@ public static String getJSString(long addr) { internal_native_copyToByteBuffer((int)source.native_void_address, destinationArray, offset, sizeInBytes); } */ + /*[-FFM;-REPLACE_BLOCK] + { + java.lang.foreign.MemorySegment seg = java.lang.foreign.MemorySegment.ofBuffer(destination); + internal_native_copyToByteBuffer(source.native_void_address, seg.address(), offset, sizeInBytes); + } + */ public static void copyToByteBuffer(IDLBase source, ByteBuffer destination, int offset, int sizeInBytes) { internal_native_copyToByteBuffer(source.native_void_address, destination, offset, sizeInBytes); } @@ -39,5 +45,29 @@ public static void copyToByteBuffer(IDLBase source, ByteBuffer destination, int char* bufferAddress = (char*)env->GetDirectBufferAddress(destination); memcpy(bufferAddress + offset, data, sizeInBytes); */ + /*[-FFM;-NATIVE] + #include + extern "C" { + FFM_EXPORT void jparser_com_github_xpenatan_jparser_idl_helper_IDLUtils_internal_1native_1copyToByteBuffer__JJII(int64_t data_addr, int64_t destination_addr, int32_t offset, int32_t sizeInBytes) { + void* data = (void*)data_addr; + char* bufferAddress = (char*)destination_addr; + memcpy(bufferAddress + offset, data, sizeInBytes); + } + } + */ + /*[-FFM;-REPLACE] + public static void internal_native_copyToByteBuffer(long data_addr, long destination_addr, int offset, int sizeInBytes) { + try { + FFMHandles.internal_native_copyToByteBuffer.invokeExact(data_addr, destination_addr, offset, sizeInBytes); + } catch(Throwable e) { throw new RuntimeException(e); } + } + */ + /*[-FFM;-ADD] + private static final class FFMHandles { + private static final java.lang.foreign.SymbolLookup LOOKUP = java.lang.foreign.SymbolLookup.loaderLookup(); + private static final java.lang.foreign.Linker LINKER = java.lang.foreign.Linker.nativeLinker(); + static final java.lang.invoke.MethodHandle internal_native_copyToByteBuffer = LINKER.downcallHandle(LOOKUP.find("jparser_com_github_xpenatan_jparser_idl_helper_IDLUtils_internal_1native_1copyToByteBuffer__JJII").orElseThrow(), java.lang.foreign.FunctionDescriptor.ofVoid(java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_LONG, java.lang.foreign.ValueLayout.JAVA_INT, java.lang.foreign.ValueLayout.JAVA_INT)); + } + */ public static native void internal_native_copyToByteBuffer(long data_addr, ByteBuffer destination, int offset, int sizeInBytes); } \ No newline at end of file diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java index d84f2832..71fa8922 100644 --- a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMCodeParser.java @@ -463,19 +463,25 @@ public void onIDLCallbackGenerated(JParser jParser, IDLClass idlClass, String methodName = idlMethod.getCPPName(); String internalMethodName = internalMethod.getNameAsString(); + // FFM upcall stubs require MethodHandle types to exactly match the FunctionDescriptor. + // For String (const char*) parameters, the native side passes a pointer (ADDRESS layout), + // so the internal method must accept MemorySegment instead of String and convert it. + fixupCallbackStringParams(internalMethod); + String methodTypeStr = buildMethodTypeStr(internalMethod); String funcDescriptor = buildCallbackFunctionDescriptor(internalMethod); body.append(" java.lang.invoke.MethodHandle mh_").append(methodName) - .append(" = java.lang.invoke.MethodHandles.lookup().findVirtual(this.getClass(), \"") + .append(" = java.lang.invoke.MethodHandles.lookup().findVirtual(") + .append(classDeclaration.getNameAsString()).append(".class, \"") .append(internalMethodName).append("\", ").append(methodTypeStr).append(").bindTo(this);\n"); body.append(" java.lang.foreign.MemorySegment stub_").append(methodName) .append(" = java.lang.foreign.Linker.nativeLinker().upcallStub(mh_").append(methodName) .append(", ").append(funcDescriptor).append(", java.lang.foreign.Arena.ofAuto());\n"); } - // Call native setupCallback with cPointer + stub addresses - body.append(" ").append(nativeMethodDeclaration.getNameAsString()).append("(cPointer"); + // Call native setupCallback with native_address + stub addresses + body.append(" ").append(nativeMethodDeclaration.getNameAsString()).append("(native_address"); for(Pair> pair : methods) { IDLMethod idlMethod = pair.a; body.append(", stub_").append(idlMethod.getCPPName()).append(".address()"); @@ -519,10 +525,10 @@ protected void setJavaBodyNativeCMD(String content, MethodDeclaration methodDecl cppGenerator.addNativeCode(methodDeclaration, content); // Register the MethodHandle entry for this native method - registerNativeMethod(methodDeclaration); + String handleName = registerNativeMethod(methodDeclaration); // Transform the native method into an FFM bridge method - convertToFFMBridgeMethod(methodDeclaration); + convertToFFMBridgeMethod(methodDeclaration, handleName); } // ==================== Lifecycle Hooks ==================== @@ -553,12 +559,22 @@ public void onParserComplete(JParser jParser, ArrayList parserItems if(parserItem.notAllowed) continue; ClassOrInterfaceDeclaration classDeclaration = parserItem.getClassDeclaration(); - if(classDeclaration == null) continue; - - String className = classDeclaration.getNameAsString(); - if(!registry.hasEntries(className)) continue; + if(classDeclaration != null) { + String className = classDeclaration.getNameAsString(); + if(registry.hasEntries(className)) { + injectFFMHandlesClass(parserItem.unit, classDeclaration, className); + } + continue; + } - injectFFMHandlesClass(parserItem.unit, classDeclaration, className); + // Also handle enum declarations (they can have native methods too) + EnumDeclaration enumDeclaration = parserItem.getEnumDeclaration(); + if(enumDeclaration != null) { + String className = enumDeclaration.getNameAsString(); + if(registry.hasEntries(className)) { + injectFFMHandlesClassForEnum(parserItem.unit, enumDeclaration, className); + } + } } } @@ -566,8 +582,9 @@ public void onParserComplete(JParser jParser, ArrayList parserItems /** * Register a native method in the MethodHandle registry. + * Returns the unique handle name (method name + overload suffix) for use in bridge method body. */ - private void registerNativeMethod(MethodDeclaration methodDeclaration) { + private String registerNativeMethod(MethodDeclaration methodDeclaration) { TypeDeclaration classOrEnum = (TypeDeclaration) methodDeclaration.getParentNode().get(); CompilationUnit compilationUnit = classOrEnum.findCompilationUnit().get(); String packageName = compilationUnit.getPackageDeclaration().get().getNameAsString(); @@ -591,17 +608,27 @@ private void registerNativeMethod(MethodDeclaration methodDeclaration) { } } + // Build overload suffix for unique handle name + StringBuilder overloadSuffix = new StringBuilder(); + for(FFMCppGenerator.FFMArgument arg : ffmArgs) { + overloadSuffix.append(arg.overloadSuffix); + } + String handleName = methodName + "__" + overloadSuffix; + String returnType = methodDeclaration.getType().toString(); String symbolName = FFMCppGenerator.buildSymbolName(packageName, className, methodName, ffmArgs); - registry.register(className, symbolName, methodName, returnType, paramInfos); + registry.register(className, symbolName, methodName, handleName, returnType, paramInfos); + return handleName; } /** * Transform a JNI-style native method declaration into an FFM bridge method. * Removes the 'native' modifier and adds a body that invokes the MethodHandle. + * + * @param handleName the unique field name in FFMHandles (includes overload suffix) */ - private void convertToFFMBridgeMethod(MethodDeclaration methodDeclaration) { + private void convertToFFMBridgeMethod(MethodDeclaration methodDeclaration, String handleName) { // Remove native modifier methodDeclaration.removeModifier(Modifier.Keyword.NATIVE); @@ -635,12 +662,19 @@ private void convertToFFMBridgeMethod(MethodDeclaration methodDeclaration) { bodyCode.append(" try {\n"); if(isVoid) { - bodyCode.append(" FFMHandles.").append(methodName) + bodyCode.append(" FFMHandles.").append(handleName) .append(".invokeExact(").append(invokeArgs).append(");\n"); } + else if(FFMTypeMapper.isString(returnTypeStr)) { + // String returns: native function returns const char* (ADDRESS). + // invokeExact returns MemorySegment — convert to Java String. + bodyCode.append(" java.lang.foreign.MemorySegment _retSeg = (java.lang.foreign.MemorySegment) FFMHandles.").append(handleName) + .append(".invokeExact(").append(invokeArgs).append(");\n"); + bodyCode.append(" return _retSeg.reinterpret(Long.MAX_VALUE).getString(0);\n"); + } else { String castType = FFMTypeMapper.getFFMCast(returnTypeStr); - bodyCode.append(" return (").append(castType).append(") FFMHandles.").append(methodName) + bodyCode.append(" return (").append(castType).append(") FFMHandles.").append(handleName) .append(".invokeExact(").append(invokeArgs).append(");\n"); } @@ -662,10 +696,37 @@ private void convertToFFMBridgeMethod(MethodDeclaration methodDeclaration) { * Inject the FFMHandles inner class into a Java class with all MethodHandle field declarations. */ private void injectFFMHandlesClass(CompilationUnit unit, ClassOrInterfaceDeclaration classDeclaration, String className) { + String innerClassSource = buildFFMHandlesSource(className); + if(innerClassSource == null) return; + + ClassOrInterfaceDeclaration innerClass = StaticJavaParser.parseBodyDeclaration(innerClassSource) + .asClassOrInterfaceDeclaration(); + classDeclaration.addMember(innerClass); + + addFFMImports(unit); + } + + /** + * Inject the FFMHandles inner class into an enum declaration. + */ + private void injectFFMHandlesClassForEnum(CompilationUnit unit, EnumDeclaration enumDeclaration, String className) { + String innerClassSource = buildFFMHandlesSource(className); + if(innerClassSource == null) return; + + ClassOrInterfaceDeclaration innerClass = StaticJavaParser.parseBodyDeclaration(innerClassSource) + .asClassOrInterfaceDeclaration(); + enumDeclaration.addMember(innerClass); + + addFFMImports(unit); + } + + /** + * Build the FFMHandles inner class source code for a given class name. + */ + private String buildFFMHandlesSource(String className) { List entries = registry.getEntries(className); - if(entries.isEmpty()) return; + if(entries.isEmpty()) return null; - // Build the inner class source StringBuilder sb = new StringBuilder(); sb.append("private static final class FFMHandles {\n"); sb.append(" private static final java.lang.foreign.SymbolLookup LOOKUP;\n"); @@ -676,20 +737,17 @@ private void injectFFMHandlesClass(CompilationUnit unit, ClassOrInterfaceDeclara for(FFMMethodHandleRegistry.FFMEntry entry : entries) { String descriptor = FFMMethodHandleRegistry.buildFunctionDescriptor(entry); - sb.append(" static final java.lang.invoke.MethodHandle ").append(entry.javaMethodName) + sb.append(" static final java.lang.invoke.MethodHandle ").append(entry.handleName) .append(" = LINKER.downcallHandle(\n"); sb.append(" LOOKUP.find(\"").append(entry.symbolName).append("\").orElseThrow(),\n"); sb.append(" ").append(descriptor).append(");\n\n"); } sb.append("}"); + return sb.toString(); + } - // Parse and add to the class - ClassOrInterfaceDeclaration innerClass = StaticJavaParser.parseBodyDeclaration(sb.toString()) - .asClassOrInterfaceDeclaration(); - classDeclaration.addMember(innerClass); - - // Add FFM imports + private void addFFMImports(CompilationUnit unit) { unit.addImport("java.lang.foreign.FunctionDescriptor"); unit.addImport("java.lang.foreign.ValueLayout"); unit.addImport("java.lang.foreign.Linker"); @@ -755,10 +813,8 @@ private void generateFFMCPPClass(IDLClass idlClass, ClassOrInterfaceDeclaration cppClass.append("};\n"); - // Emit the C++ class via the generator (before extern "C") - cppGenerator.addCallbackClassCode(cppClass.toString()); - - // Also attach to constructor block comment (same pattern as CppCodeParser) + // Attach to constructor block comment (same pattern as CppCodeParser). + // parseCodeBlock will emit the code via cppGenerator.addNativeCode(). String header = "[-" + HEADER_CMD + ";" + CMD_NATIVE + "]\n"; String code = header + cppClass.toString(); classDeclaration.getConstructors().get(0).setBlockComment(code); @@ -823,6 +879,11 @@ private String generateFFMMethodCallers(IDLClass idlClass, tag = "& "; callParamCast = "(int64_t)&"; } + else if(idlParameter.isAny) { + // any type = void* in C++ virtual method; needs (int64_t) cast for function pointer + // Don't change tag — getCPPType() already returns "void*" + callParamCast = "(int64_t)"; + } else if(!idlParameter.isEnum() && !isPrimitive && !idlParameter.isValue) { tag = "* "; callParamCast = "(int64_t)"; @@ -868,8 +929,14 @@ private String buildFPTypedef(String className, IDLMethod idlMethod, MethodDecla NodeList params = internalMethod.getParameters(); for(int i = 0; i < params.size(); i++) { if(i > 0) sb.append(", "); - String paramType = params.get(i).getType().asString(); - sb.append(FFMTypeMapper.getCType(paramType)); + // Use IDL parameter info to detect string (DOMString) types, since + // fixupCallbackStringParams may have changed the Java type to MemorySegment. + if(i < idlMethod.parameters.size() && idlMethod.parameters.get(i).idlType.equals("DOMString")) { + sb.append("const char*"); + } else { + String paramType = params.get(i).getType().asString(); + sb.append(FFMTypeMapper.getCType(paramType)); + } } sb.append(");"); return sb.toString(); @@ -888,6 +955,29 @@ private String buildFPTypeName(String className, IDLMethod idlMethod, MethodDecl return "fp_" + className + "_" + idlMethod.getCPPName() + "_" + suffix; } + /** + * Fix up String parameters on a callback internal method for FFM upcall compatibility. + * Changes the parameter type from String to MemorySegment and inserts conversion code + * at the start of the method body (MemorySegment → String via getString(0)). + */ + private void fixupCallbackStringParams(MethodDeclaration internalMethod) { + NodeList params = internalMethod.getParameters(); + for(int i = 0; i < params.size(); i++) { + Parameter param = params.get(i); + if(param.getType().asString().equals("String")) { + String originalName = param.getNameAsString(); + String segmentName = originalName + "_seg"; + param.setName(segmentName); + param.setType(StaticJavaParser.parseType("java.lang.foreign.MemorySegment")); + // Insert conversion statement at the top of the method body + String convStmt = "String " + originalName + " = " + segmentName + + ".reinterpret(Long.MAX_VALUE).getString(0);"; + internalMethod.getBody().ifPresent(body -> + body.getStatements().add(0, StaticJavaParser.parseStatement(convStmt))); + } + } + } + /** * Build MethodType string for MethodHandles.lookup().findVirtual(). * Example: java.lang.invoke.MethodType.methodType(void.class, long.class, long.class) diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java index c2059d04..566aecc9 100644 --- a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMMethodHandleRegistry.java @@ -19,9 +19,9 @@ public class FFMMethodHandleRegistry { * Register a native method for a given class. */ public void register(String className, String symbolName, String javaMethodName, - String returnType, List parameters) { + String handleName, String returnType, List parameters) { List entries = classEntries.computeIfAbsent(className, k -> new ArrayList<>()); - entries.add(new FFMEntry(symbolName, javaMethodName, returnType, parameters)); + entries.add(new FFMEntry(symbolName, javaMethodName, handleName, returnType, parameters)); } /** @@ -100,12 +100,15 @@ public static String buildFunctionDescriptor(FFMEntry entry) { public static class FFMEntry { public final String symbolName; public final String javaMethodName; + /** Unique field name for the MethodHandle in FFMHandles (includes overload suffix). */ + public final String handleName; public final String returnType; public final List parameters; - public FFMEntry(String symbolName, String javaMethodName, String returnType, List parameters) { + public FFMEntry(String symbolName, String javaMethodName, String handleName, String returnType, List parameters) { this.symbolName = symbolName; this.javaMethodName = javaMethodName; + this.handleName = handleName; this.returnType = returnType; this.parameters = parameters; } diff --git a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java index eff86110..29541210 100644 --- a/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java +++ b/jParser/jParser-ffm/src/main/java/com/github/xpenatan/jParser/ffm/FFMTypeMapper.java @@ -23,6 +23,7 @@ public class FFMTypeMapper { javaToValueLayout.put("byte", "ValueLayout.JAVA_BYTE"); javaToValueLayout.put("char", "ValueLayout.JAVA_CHAR"); javaToValueLayout.put("String", "ValueLayout.ADDRESS"); + javaToValueLayout.put("java.lang.foreign.MemorySegment", "ValueLayout.ADDRESS"); // Array types → ADDRESS layout (passed as MemorySegment pointers) javaToValueLayout.put("int[]", "ValueLayout.ADDRESS"); diff --git a/settings.gradle.kts b/settings.gradle.kts index 468ed56b..114a3ac0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ include(":idl-helper:idl-helper-build") include(":idl-helper:idl-helper-core") include(":idl-helper:idl-helper-teavm") include(":idl-helper:idl-helper-desktop") +include(":idl-helper:idl-helper-ffm") include(":idl-helper:idl-helper-android") include(":loader:loader-core") @@ -36,6 +37,7 @@ include(":examples:TestLib:app:android") include(":examples:SharedLib:libA:lib-build") include(":examples:SharedLib:libA:lib-base") include(":examples:SharedLib:libA:lib-core") +include(":examples:SharedLib:libA:lib-ffm") include(":examples:SharedLib:libA:lib-desktop") include(":examples:SharedLib:libA:lib-teavm") include(":examples:SharedLib:libA:lib-android") @@ -43,6 +45,7 @@ include(":examples:SharedLib:libA:lib-android") include(":examples:SharedLib:libB:lib-build") include(":examples:SharedLib:libB:lib-base") include(":examples:SharedLib:libB:lib-core") +include(":examples:SharedLib:libB:lib-ffm") include(":examples:SharedLib:libB:lib-desktop") include(":examples:SharedLib:libB:lib-teavm") include(":examples:SharedLib:libB:lib-android") From 07494e70f74b0b09fae5d062f35a8a93356ab092 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 24 Mar 2026 21:05:08 -0300 Subject: [PATCH 07/12] Update workflow --- .github/workflows/build_and_upload.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/build_and_upload.yml b/.github/workflows/build_and_upload.yml index 614cce03..2e65ac52 100644 --- a/.github/workflows/build_and_upload.yml +++ b/.github/workflows/build_and_upload.yml @@ -53,6 +53,9 @@ jobs: - name: Build idl-helper project run: ./gradlew idl_helper_build_project_windows64 + - name: Build idl-helper FFM project + run: ./gradlew idl_helper_build_project_ffm_windows64 + - name: Build TestLib project run: ./gradlew TestLib_build_project_windows64 @@ -100,6 +103,9 @@ jobs: - name: Build idl-helper project run: ./gradlew idl_helper_build_project_linux64 + - name: Build idl-helper FFM project + run: ./gradlew idl_helper_build_project_ffm_linux64 + - name: Build TestLib project run: ./gradlew TestLib_build_project_linux64 @@ -143,6 +149,9 @@ jobs: - name: Build project idl-helper run: ./gradlew idl_helper_build_project_mac64 + - name: Build idl-helper FFM project + run: ./gradlew idl_helper_build_project_ffm_mac64 + - name: Build TestLib project run: | ./gradlew TestLib_build_project_mac64 @@ -189,6 +198,9 @@ jobs: - name: Build project idl-helper run: ./gradlew idl_helper_build_project_macArm + - name: Build idl-helper FFM project + run: ./gradlew idl_helper_build_project_ffm_macArm + - name: Build TestLib project run: | ./gradlew TestLib_build_project_macArm From 429db8ba212e7f749722430110dd910feb7756a5 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 24 Mar 2026 22:00:55 -0300 Subject: [PATCH 08/12] Update modules --- .github/workflows/build_and_upload.yml | 78 +++++++--- .gitignore | 12 +- AGENTS.md | 66 +++++---- buildSrc/src/main/kotlin/LibExt.kt | 4 +- buildSrc/src/main/kotlin/publish.gradle.kts | 4 +- examples/SharedLib/app/core/build.gradle.kts | 12 +- .../{desktop => desktop-ffm}/build.gradle.kts | 15 +- .../xpenatan/jParser/example/app/Main.java | 0 .../app/desktop-jni/build.gradle.kts | 34 +++++ .../xpenatan/jParser/example/app/Main.java | 12 ++ examples/SharedLib/app/teavm/build.gradle.kts | 20 +-- .../app/teavm/src/main/java/Build.java | 35 ++--- .../teavm/src/main/java/TeaVMLauncher.java | 8 +- .../SharedLib/libA/lib-build/build.gradle.kts | 12 +- .../lib-desktop-ffm}/build.gradle.kts | 23 ++- .../build.gradle.kts | 2 +- .../SharedLib/libB/lib-build/build.gradle.kts | 12 +- .../lib-desktop-ffm}/build.gradle.kts | 27 ++-- .../build.gradle.kts | 2 +- .../{desktop => }/assets/data/badlogic.jpg | Bin examples/TestLib/app/core/build.gradle.kts | 7 +- .../{desktop => desktop-ffm}/build.gradle.kts | 137 ++++-------------- .../jParser/example/app/BenchmarkMain.java | 0 .../xpenatan/jParser/example/app/Main.java | 0 .../app/NativeBridgeBenchmarkMain.java | 0 .../app/NativeBridgeFpsBenchmarkMain.java | 0 .../TestLib/app/desktop-jni/build.gradle.kts | 81 +++++++++++ .../jParser/example/app/BenchmarkMain.java | 12 ++ .../xpenatan/jParser/example/app/Main.java | 12 ++ .../app/NativeBridgeBenchmarkMain.java | 14 ++ .../app/NativeBridgeFpsBenchmarkMain.java | 16 ++ examples/TestLib/app/teavm/build.gradle.kts | 31 +--- .../teavm/src/main/java/BenchmarkBuild.java | 27 ++-- .../src/main/java/BenchmarkLauncher.java | 8 +- .../app/teavm/src/main/java/Build.java | 35 ++--- .../teavm/src/main/java/TeaVMLauncher.java | 8 +- .../TestLib/lib/lib-build/build.gradle.kts | 69 ++++----- .../build.gradle.kts | 28 ++-- .../build.gradle.kts | 6 +- .../jParser/example/NormalClassTest.java | 65 --------- idl-helper/idl-helper-build/build.gradle.kts | 12 +- .../build.gradle.kts | 17 ++- .../build.gradle.kts | 4 +- .../builder/tool/BuildToolOptions.java | 2 +- settings.gradle.kts | 22 +-- 45 files changed, 526 insertions(+), 465 deletions(-) rename examples/SharedLib/app/{desktop => desktop-ffm}/build.gradle.kts (64%) rename examples/SharedLib/app/{desktop => desktop-ffm}/src/main/java/com/github/xpenatan/jParser/example/app/Main.java (100%) create mode 100644 examples/SharedLib/app/desktop-jni/build.gradle.kts create mode 100644 examples/SharedLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java rename examples/SharedLib/{libB/lib-ffm => libA/lib-desktop-ffm}/build.gradle.kts (60%) rename examples/SharedLib/libA/{lib-desktop => lib-desktop-jni}/build.gradle.kts (93%) rename examples/SharedLib/{libA/lib-ffm => libB/lib-desktop-ffm}/build.gradle.kts (57%) rename examples/SharedLib/libB/{lib-desktop => lib-desktop-jni}/build.gradle.kts (93%) rename examples/TestLib/app/{desktop => }/assets/data/badlogic.jpg (100%) rename examples/TestLib/app/{desktop => desktop-ffm}/build.gradle.kts (50%) rename examples/TestLib/app/{desktop => desktop-ffm}/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java (100%) rename examples/TestLib/app/{desktop => desktop-ffm}/src/main/java/com/github/xpenatan/jParser/example/app/Main.java (100%) rename examples/TestLib/app/{desktop => desktop-ffm}/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java (100%) rename examples/TestLib/app/{desktop => desktop-ffm}/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java (100%) create mode 100644 examples/TestLib/app/desktop-jni/build.gradle.kts create mode 100644 examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java create mode 100644 examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java create mode 100644 examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java create mode 100644 examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java rename examples/TestLib/lib/{lib-ffm => lib-desktop-ffm}/build.gradle.kts (55%) rename examples/TestLib/lib/{lib-desktop => lib-desktop-jni}/build.gradle.kts (90%) delete mode 100644 examples/TestLib/lib/lib-desktop/src/test/java/com/github/xpenatan/jParser/example/NormalClassTest.java rename idl-helper/{idl-helper-ffm => idl-helper-desktop-ffm}/build.gradle.kts (70%) rename idl-helper/{idl-helper-desktop => idl-helper-desktop-jni}/build.gradle.kts (90%) diff --git a/.github/workflows/build_and_upload.yml b/.github/workflows/build_and_upload.yml index 2e65ac52..55d366da 100644 --- a/.github/workflows/build_and_upload.yml +++ b/.github/workflows/build_and_upload.yml @@ -51,19 +51,28 @@ jobs: value: $env:PATH;C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build - name: Build idl-helper project - run: ./gradlew idl_helper_build_project_windows64 + run: ./gradlew idl_helper_build_project_jni_windows64 - name: Build idl-helper FFM project run: ./gradlew idl_helper_build_project_ffm_windows64 - name: Build TestLib project - run: ./gradlew TestLib_build_project_windows64 + run: ./gradlew TestLib_build_project_jni_windows64 + + - name: Build TestLib FFM project + run: ./gradlew TestLib_build_project_ffm_windows64 - name: Build Shared LibA project - run: ./gradlew LibA_build_project_windows64 + run: ./gradlew LibA_build_project_jni_windows64 + + - name: Build Shared LibA FFM project + run: ./gradlew LibA_build_project_ffm_windows64 - name: Build Shared LibB project - run: ./gradlew LibB_build_project_windows64 + run: ./gradlew LibB_build_project_jni_windows64 + + - name: Build Shared LibB FFM project + run: ./gradlew LibB_build_project_ffm_windows64 # - name: Test Shared Lib # run: ./gradlew :examples:SharedLib:app:core:test @@ -101,19 +110,28 @@ jobs: run: chmod +x ./gradlew - name: Build idl-helper project - run: ./gradlew idl_helper_build_project_linux64 + run: ./gradlew idl_helper_build_project_jni_linux64 - name: Build idl-helper FFM project run: ./gradlew idl_helper_build_project_ffm_linux64 - name: Build TestLib project - run: ./gradlew TestLib_build_project_linux64 + run: ./gradlew TestLib_build_project_jni_linux64 + + - name: Build TestLib FFM project + run: ./gradlew TestLib_build_project_ffm_linux64 - name: Build Shared LibA project - run: ./gradlew LibA_build_project_linux64 + run: ./gradlew LibA_build_project_jni_linux64 + + - name: Build Shared LibA FFM project + run: ./gradlew LibA_build_project_ffm_linux64 - name: Build Shared LibB project - run: ./gradlew LibB_build_project_linux64 + run: ./gradlew LibB_build_project_jni_linux64 + + - name: Build Shared LibB FFM project + run: ./gradlew LibB_build_project_ffm_linux64 # - name: Test Shared Lib # run: ./gradlew :examples:SharedLib:app:core:test @@ -147,22 +165,34 @@ jobs: run: chmod +x ./gradlew - name: Build project idl-helper - run: ./gradlew idl_helper_build_project_mac64 + run: ./gradlew idl_helper_build_project_jni_mac64 - name: Build idl-helper FFM project run: ./gradlew idl_helper_build_project_ffm_mac64 - name: Build TestLib project run: | - ./gradlew TestLib_build_project_mac64 + ./gradlew TestLib_build_project_jni_mac64 + + - name: Build TestLib FFM project + run: | + ./gradlew TestLib_build_project_ffm_mac64 # - name: Build Shared LibA project # run: | -# ./gradlew LibA_build_project_mac64 +# ./gradlew LibA_build_project_jni_mac64 +# +# - name: Build Shared LibA FFM project +# run: | +# ./gradlew LibA_build_project_ffm_mac64 # # - name: Build Shared LibB project # run: | -# ./gradlew LibB_build_project_mac64 +# ./gradlew LibB_build_project_jni_mac64 +# +# - name: Build Shared LibB FFM project +# run: | +# ./gradlew LibB_build_project_ffm_mac64 # - name: Test Shared Lib # run: ./gradlew :examples:SharedLib:app:core:test @@ -196,22 +226,34 @@ jobs: run: chmod +x ./gradlew - name: Build project idl-helper - run: ./gradlew idl_helper_build_project_macArm + run: ./gradlew idl_helper_build_project_jni_macArm - name: Build idl-helper FFM project run: ./gradlew idl_helper_build_project_ffm_macArm - name: Build TestLib project run: | - ./gradlew TestLib_build_project_macArm + ./gradlew TestLib_build_project_jni_macArm + + - name: Build TestLib FFM project + run: | + ./gradlew TestLib_build_project_ffm_macArm # - name: Build Shared LibA project # run: | -# ./gradlew LibA_build_project_macArm +# ./gradlew LibA_build_project_jni_macArm +# +# - name: Build Shared LibA FFM project +# run: | +# ./gradlew LibA_build_project_ffm_macArm # # - name: Build Shared LibB project # run: | -# ./gradlew LibB_build_project_macArm +# ./gradlew LibB_build_project_jni_macArm +# +# - name: Build Shared LibB FFM project +# run: | +# ./gradlew LibB_build_project_ffm_macArm # - name: Test Shared Lib # run: ./gradlew :examples:SharedLib:app:core:test @@ -301,12 +343,12 @@ jobs: run: chmod +x ./gradlew - name: Build project idl-helper - run: ./gradlew idl_helper_build_project_android + run: ./gradlew idl_helper_build_project_jni_android env: NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - name: Build project - run: ./gradlew TestLib_build_project_android + run: ./gradlew TestLib_build_project_jni_android env: NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} diff --git a/.gitignore b/.gitignore index 961476b5..9f1c56f5 100644 --- a/.gitignore +++ b/.gitignore @@ -31,20 +31,24 @@ out/ **/idl-helper/idl-helper-teavm/src/main/java/** **/idl-helper/idl-helper-core/src/main/java/** -**/idl-helper/idl-helper-ffm/src/main/java/** +**/idl-helper/idl-helper-desktop-ffm/src/main/java/** +**/idl-helper/idl-helper-desktop-jni/src/main/java/** **/webapp/** **/lib/core/src/** **/lib/desktop/src/main/** **/lib/lib-teavm/src/main/java/** **/lib/lib-core/src/main/java/** -**/lib/lib-ffm/src/main/java/** +**/lib/lib-desktop-ffm/src/main/java/** +**/lib/lib-desktop-jni/src/main/java/** **/libA/lib-teavm/src/main/java/** **/libA/lib-core/src/main/java/** -**/libA/lib-ffm/src/main/java/** +**/libA/lib-desktop-ffm/src/main/java/** +**/libA/lib-desktop-jni/src/main/java/** **/libB/lib-teavm/src/main/java/** **/libB/lib-core/src/main/java/** -**/libB/lib-ffm/src/main/java/** +**/libB/lib-desktop-ffm/src/main/java/** +**/libB/lib-desktop-jni/src/main/java/** **/app/android/libs/ .codiumai .kotlin/ diff --git a/AGENTS.md b/AGENTS.md index f914ffca..ce696e72 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,7 +37,8 @@ This ensures that if the session is interrupted, the next agent has a perfect "s | `lib-build` | `BuildLib.main()` entry point — configures IDL, targets, runs generation + compilation | Java 11 | | `lib-core` | **Generated** JNI Java output (do not hand-edit) | Java 11 | | `lib-teavm` | **Generated** TeaVM Java output (do not hand-edit) | Java 11 | -| `lib-desktop` | Runtime loader for desktop | — | +| `lib-desktop-jni` | Bundles JNI-compiled native DLLs/shared-libs into a JAR (no Java code) | — | +| `lib-desktop-ffm` | **Generated** FFM Java output + FFM-compiled native DLLs/shared-libs (do not hand-edit) | Java 22+ | | `lib-android` | Android-specific packaging | — | This pattern repeats across `jParser/`, `idl-helper/`, `loader/`, and `examples/`. @@ -89,10 +90,10 @@ The idl-helper provides the `IDLBase` runtime for all native-bound objects. It m ```sh # Step 1 — Generate JNI/TeaVM/FFM Java code + compile native library for your platform # JNI (pick your platform): -./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_windows64 -./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_linux64 -./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_mac64 -./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_macArm +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_jni_windows64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_jni_linux64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_jni_mac64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_jni_macArm # FFM (pick your platform): ./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_ffm_windows64 @@ -106,19 +107,19 @@ The idl-helper provides the `IDLBase` runtime for all native-bound objects. It m ### 2. Build example: TestLib -TestLib is the primary test/example library. Building it generates Java source code into `lib-core`, `lib-teavm`, and `lib-ffm`, then compiles native C/C++ into platform DLLs/shared-libs. +TestLib is the primary test/example library. Building it generates Java source code into `lib-core`, `lib-teavm`, and `lib-desktop-ffm`, then compiles native C/C++ into platform DLLs/shared-libs. ```sh # Step 1 — Generate Java code + compile native for your platform (JNI): -./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_windows64 -./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_linux64 -./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_mac64 -./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_macArm +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_jni_windows64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_jni_linux64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_jni_mac64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_jni_macArm # All JNI platforms: ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_all -# FFM (generates lib-ffm Java code + compiles FFM native): +# FFM (generates lib-desktop-ffm Java code + compiles FFM native): ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_windows64 ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_linux64 ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_mac64 @@ -127,8 +128,9 @@ TestLib is the primary test/example library. Building it generates Java source c # Generate FFM Java code only (no native compilation): ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm -# Step 2 — Run the desktop app: -./gradlew :examples:TestLib:app:desktop:TestLib_run_app_desktop +# Step 2 — Run the desktop app (JNI or FFM): +./gradlew :examples:TestLib:app:desktop-jni:TestLib_run_app_desktop +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_run_app_desktop ``` ### 3. Build example: SharedLib (multi-library) @@ -137,17 +139,18 @@ SharedLib demonstrates two libraries (libA + libB) where libB depends on libA. * ```sh # libA — JNI: -./gradlew :examples:SharedLib:libA:lib-build:LibA_build_project_windows64 +./gradlew :examples:SharedLib:libA:lib-build:LibA_build_project_jni_windows64 # libA — FFM: ./gradlew :examples:SharedLib:libA:lib-build:LibA_build_project_ffm_windows64 # libB — JNI (after libA): -./gradlew :examples:SharedLib:libB:lib-build:LibB_build_project_windows64 +./gradlew :examples:SharedLib:libB:lib-build:LibB_build_project_jni_windows64 # libB — FFM (after libA): ./gradlew :examples:SharedLib:libB:lib-build:LibB_build_project_ffm_windows64 -# Run SharedLib desktop app: -./gradlew :examples:SharedLib:app:desktop:SharedLib_run_app_desktop +# Run SharedLib desktop app (JNI or FFM): +./gradlew :examples:SharedLib:app:desktop-jni:SharedLib_run_app_desktop +./gradlew :examples:SharedLib:app:desktop-ffm:SharedLib_run_app_desktop ``` Replace `windows64` with `linux64`, `mac64`, or `macArm` for other platforms. @@ -158,13 +161,13 @@ Run micro-benchmarks comparing JNI and FFM bridge overhead. **Requires both JNI ```sh # Run both benchmarks and print a comparison table -./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_compare +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_benchmark_compare # Run only JNI benchmark (saves CSV to build/benchmark/benchmark_jni.csv) -./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_jni +./gradlew :examples:TestLib:app:desktop-jni:TestLib_benchmark_jni # Run only FFM benchmark (saves CSV to build/benchmark/benchmark_ffm.csv) -./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_ffm +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_benchmark_ffm ``` ### 5. JNI vs FFM FPS Benchmarks @@ -173,43 +176,44 @@ Measures how native bridge overhead affects frame rate. Each frame executes a fi ```sh # Run both FPS benchmarks and print a comparison table -./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_compare +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_fps_benchmark_compare # Run only JNI FPS benchmark (saves CSV to build/benchmark/fps_benchmark_jni.csv) -./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_jni +./gradlew :examples:TestLib:app:desktop-jni:TestLib_fps_benchmark_jni # Run only FFM FPS benchmark (saves CSV to build/benchmark/fps_benchmark_ffm.csv) -./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_ffm +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_fps_benchmark_ffm ``` ### Build order summary (from scratch on Windows) ```sh # 1. Build idl-helper native (both JNI + FFM) -./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_windows64 +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_jni_windows64 ./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_ffm_windows64 # 2. Build TestLib native (both JNI + FFM) -./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_windows64 +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_jni_windows64 ./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_ffm_windows64 -# 3. Run the desktop app -./gradlew :examples:TestLib:app:desktop:TestLib_run_app_desktop +# 3. Run the desktop app (JNI or FFM) +./gradlew :examples:TestLib:app:desktop-jni:TestLib_run_app_desktop +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_run_app_desktop # 4. Run throughput benchmarks -./gradlew :examples:TestLib:app:desktop:TestLib_benchmark_compare +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_benchmark_compare # 5. Run FPS benchmarks -./gradlew :examples:TestLib:app:desktop:TestLib_fps_benchmark_compare +./gradlew :examples:TestLib:app:desktop-ffm:TestLib_fps_benchmark_compare ``` ## Conventions - **Version management**: `gradle.properties` holds the version; `LibExt.kt` in `buildSrc/` resolves it. Snapshots use `"-SNAPSHOT"`, releases use the property value. - **Publishing**: The `publish.gradle.kts` plugin configures all library modules listed in `libProjects`. Use `publishRelease` or `publishTestRelease` tasks. -- **Generated code is not hand-edited**: `lib-core/`, `lib-teavm/`, and `lib-ffm/` directories contain generated output with a "Do not make changes" header. +- **Generated code is not hand-edited**: `lib-core/`, `lib-teavm/`, and `lib-desktop-ffm/` directories contain generated output with a "Do not make changes" header. - **IDL files** live at `lib-build/src/main/cpp/.idl`. Custom C++ glue code goes in `lib-build/src/main/cpp/custom/`. - **IDLBase** is the parent of all native-bound classes. Memory must be manually managed via `dispose()`. Use `ClassName.NULL` instead of Java `null` for native parameters. - **Dependencies**: JavaParser (`3.26.1`) for AST manipulation, TeaVM (`0.13.0`) for web target, JUnit 4 for tests. -- **Native bridge selection**: Desktop modules choose `lib-core` (JNI) or `lib-ffm` (FFM) via the `implementation` dependency in `app/desktop/build.gradle.kts`. Only one should be active at a time. +- **Native bridge selection**: Each example has separate `app/desktop-jni` and `app/desktop-ffm` modules. JNI uses `lib-core` + `lib-desktop-jni`, FFM uses `lib-desktop-ffm`. diff --git a/buildSrc/src/main/kotlin/LibExt.kt b/buildSrc/src/main/kotlin/LibExt.kt index efba2e29..fe8231ca 100644 --- a/buildSrc/src/main/kotlin/LibExt.kt +++ b/buildSrc/src/main/kotlin/LibExt.kt @@ -16,14 +16,14 @@ object LibExt { // Lib Dependencies const val jniGenVersion = "2.5.1" - const val teaVMVersion = "0.13.0" + const val teaVMVersion = "0.13.1" const val javaparserVersion = "3.26.1" const val jMultiplatform = "0.1.3" // Example Dependencies const val exampleUseRepoLibs = false const val gdxVersion = "1.14.0" - const val gdxTeaVMVersion = "1.4.0" + const val gdxTeaVMVersion = "1.5.4" const val jUnitVersion = "4.13.2" } diff --git a/buildSrc/src/main/kotlin/publish.gradle.kts b/buildSrc/src/main/kotlin/publish.gradle.kts index d08a30f3..463785d6 100644 --- a/buildSrc/src/main/kotlin/publish.gradle.kts +++ b/buildSrc/src/main/kotlin/publish.gradle.kts @@ -16,8 +16,8 @@ var libProjects = mutableSetOf( project(":idl-helper:idl-helper-base"), project(":idl-helper:idl-helper-core"), project(":idl-helper:idl-helper-teavm"), - project(":idl-helper:idl-helper-desktop"), - project(":idl-helper:idl-helper-ffm"), + project(":idl-helper:idl-helper-desktop-jni"), + project(":idl-helper:idl-helper-desktop-ffm"), project(":idl-helper:idl-helper-android"), project(":loader:loader-core"), project(":loader:loader-teavm"), diff --git a/examples/SharedLib/app/core/build.gradle.kts b/examples/SharedLib/app/core/build.gradle.kts index 253d7662..36fcb787 100644 --- a/examples/SharedLib/app/core/build.gradle.kts +++ b/examples/SharedLib/app/core/build.gradle.kts @@ -9,18 +9,16 @@ java { dependencies { // compileOnly: app/core compiles against lib-core's API, but does NOT - // propagate it transitively. Each platform module (desktop, android, teavm) - // provides the actual native bridge implementation: - // - lib-core for JNI (desktop/android) - // - lib-ffm for FFM (desktop with Java 22+) + // propagate it transitively. Each platform module (desktop-jni, desktop-ffm, + // android, teavm) provides the actual native bridge implementation. compileOnly(project(":examples:SharedLib:libA:lib-core")) compileOnly(project(":examples:SharedLib:libB:lib-core")) implementation("com.badlogicgames.gdx:gdx:${LibExt.gdxVersion}") - testImplementation(project(":examples:SharedLib:libA:lib-desktop")) - testImplementation(project(":examples:SharedLib:libB:lib-desktop")) - testImplementation(project(":idl-helper:idl-helper-desktop")) + testImplementation(project(":examples:SharedLib:libA:lib-desktop-jni")) + testImplementation(project(":examples:SharedLib:libB:lib-desktop-jni")) + testImplementation(project(":idl-helper:idl-helper-desktop-jni")) testImplementation("junit:junit:${LibExt.jUnitVersion}") } diff --git a/examples/SharedLib/app/desktop/build.gradle.kts b/examples/SharedLib/app/desktop-ffm/build.gradle.kts similarity index 64% rename from examples/SharedLib/app/desktop/build.gradle.kts rename to examples/SharedLib/app/desktop-ffm/build.gradle.kts index b39b8fcf..57e17e6c 100644 --- a/examples/SharedLib/app/desktop/build.gradle.kts +++ b/examples/SharedLib/app/desktop-ffm/build.gradle.kts @@ -11,16 +11,8 @@ java { dependencies { implementation(project(":examples:SharedLib:app:core")) - - // Choose ONE native bridge per lib — lib-core (JNI) or lib-ffm (FFM). - // implementation(project(":examples:SharedLib:libA:lib-core")) // JNI (default) - // implementation(project(":examples:SharedLib:libB:lib-core")) // JNI (default) - implementation(project(":examples:SharedLib:libA:lib-ffm")) // FFM (Java 22+, no JNI overhead) - implementation(project(":examples:SharedLib:libB:lib-ffm")) // FFM (Java 22+, no JNI overhead) - - implementation(project(":examples:SharedLib:libA:lib-desktop")) - implementation(project(":examples:SharedLib:libB:lib-desktop")) - implementation(project(":idl-helper:idl-helper-desktop")) + implementation(project(":examples:SharedLib:libA:lib-desktop-ffm")) + implementation(project(":examples:SharedLib:libB:lib-desktop-ffm")) implementation("com.badlogicgames.gdx:gdx-platform:${LibExt.gdxVersion}:natives-desktop") implementation("com.badlogicgames.gdx:gdx-backend-lwjgl3:${LibExt.gdxVersion}") @@ -28,11 +20,10 @@ dependencies { tasks.register("SharedLib_run_app_desktop") { group = "example-desktop" - description = "Run desktop app" + description = "Run desktop app (FFM)" mainClass.set("com.github.xpenatan.jParser.example.app.Main") classpath = sourceSets["main"].runtimeClasspath - // FFM requires JDK 22+ and native access javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(25)) }) diff --git a/examples/SharedLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/Main.java b/examples/SharedLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/Main.java similarity index 100% rename from examples/SharedLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/Main.java rename to examples/SharedLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/Main.java diff --git a/examples/SharedLib/app/desktop-jni/build.gradle.kts b/examples/SharedLib/app/desktop-jni/build.gradle.kts new file mode 100644 index 00000000..971a771b --- /dev/null +++ b/examples/SharedLib/app/desktop-jni/build.gradle.kts @@ -0,0 +1,34 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform + +plugins { + id("java") +} + +java { + sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) +} + +dependencies { + implementation(project(":examples:SharedLib:app:core")) + implementation(project(":examples:SharedLib:libA:lib-core")) + implementation(project(":examples:SharedLib:libB:lib-core")) + implementation(project(":examples:SharedLib:libA:lib-desktop-jni")) + implementation(project(":examples:SharedLib:libB:lib-desktop-jni")) + implementation(project(":idl-helper:idl-helper-desktop-jni")) + + implementation("com.badlogicgames.gdx:gdx-platform:${LibExt.gdxVersion}:natives-desktop") + implementation("com.badlogicgames.gdx:gdx-backend-lwjgl3:${LibExt.gdxVersion}") +} + +tasks.register("SharedLib_run_app_desktop") { + group = "example-desktop" + description = "Run desktop app (JNI)" + mainClass.set("com.github.xpenatan.jParser.example.app.Main") + classpath = sourceSets["main"].runtimeClasspath + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } +} + diff --git a/examples/SharedLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java b/examples/SharedLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java new file mode 100644 index 00000000..8801407b --- /dev/null +++ b/examples/SharedLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java @@ -0,0 +1,12 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; + +public class Main { + + public static void main(String[] args) { + Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration(); + new Lwjgl3Application(new SharedLibApp(), config); + } +} \ No newline at end of file diff --git a/examples/SharedLib/app/teavm/build.gradle.kts b/examples/SharedLib/app/teavm/build.gradle.kts index 712a5f9d..d804c3d1 100644 --- a/examples/SharedLib/app/teavm/build.gradle.kts +++ b/examples/SharedLib/app/teavm/build.gradle.kts @@ -1,12 +1,5 @@ plugins { id("java") - id("org.gretty") version("4.1.10") -} - - -project.extra["webAppDir"] = File(projectDir, "build/dist/webapp") -gretty { - contextPath = "/" } java { @@ -21,21 +14,12 @@ dependencies { implementation(project(":idl-helper:idl-helper-teavm")) implementation("com.badlogicgames.gdx:gdx:${LibExt.gdxVersion}") - implementation("com.github.xpenatan.gdx-teavm:backend-teavm:${LibExt.gdxTeaVMVersion}") + implementation("com.github.xpenatan.gdx-teavm:backend-web:${LibExt.gdxTeaVMVersion}") } -tasks.register("SharedLib_build_app_teavm") { +tasks.register("SharedLib_run_app_teavm") { group = "example-teavm" description = "Build teavm app" mainClass.set("Build") classpath = sourceSets["main"].runtimeClasspath -} - -tasks.register("SharedLib_run_app_teavm") { - group = "example-teavm" - description = "Run teavm app" - val list = listOf("SharedLib_build_app_teavm", "jettyRun") - dependsOn(list) - - tasks.findByName("jettyRun")?.mustRunAfter("SharedLib_build_app_teavm") } \ No newline at end of file diff --git a/examples/SharedLib/app/teavm/src/main/java/Build.java b/examples/SharedLib/app/teavm/src/main/java/Build.java index 2b87e183..08c46b84 100644 --- a/examples/SharedLib/app/teavm/src/main/java/Build.java +++ b/examples/SharedLib/app/teavm/src/main/java/Build.java @@ -1,32 +1,21 @@ -import com.github.xpenatan.gdx.backends.teavm.config.AssetFileHandle; -import com.github.xpenatan.gdx.backends.teavm.config.TeaBuildConfiguration; -import com.github.xpenatan.gdx.backends.teavm.config.TeaBuilder; +import com.github.xpenatan.gdx.teavm.backends.shared.config.AssetFileHandle; +import com.github.xpenatan.gdx.teavm.backends.shared.config.compiler.TeaCompiler; +import com.github.xpenatan.gdx.teavm.backends.web.config.backend.WebBackend; import java.io.File; import java.io.IOException; -import org.teavm.tooling.TeaVMSourceFilePolicy; -import org.teavm.tooling.TeaVMTargetType; -import org.teavm.tooling.TeaVMTool; -import org.teavm.tooling.sources.DirectorySourceFileProvider; import org.teavm.vm.TeaVMOptimizationLevel; public class Build { public static void main(String[] args) throws IOException { - TeaBuildConfiguration teaBuildConfiguration = new TeaBuildConfiguration(); - teaBuildConfiguration.assetsPath.add(new AssetFileHandle("../desktop/assets")); - teaBuildConfiguration.webappPath = new File("build/dist").getCanonicalPath(); - teaBuildConfiguration.targetType = TeaVMTargetType.JAVASCRIPT; - TeaBuilder.config(teaBuildConfiguration); - - TeaVMTool tool = new TeaVMTool(); - tool.setObfuscated(false); - tool.setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE); - tool.setMainClass(TeaVMLauncher.class.getName()); - -// tool.setDebugInformationGenerated(true); -// tool.setSourceMapsFileGenerated(true); -// tool.setSourceFilePolicy(TeaVMSourceFilePolicy.COPY); - - TeaBuilder.build(tool); + AssetFileHandle assetsPath = new AssetFileHandle("../assets"); + WebBackend webBackend = new WebBackend(); + webBackend.setStartJettyAfterBuild(true); + new TeaCompiler(webBackend) + .addAssets(assetsPath) + .setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE) + .setMainClass(TeaVMLauncher.class.getName()) + .setObfuscated(false) + .build(new File("build/dist")); } } diff --git a/examples/SharedLib/app/teavm/src/main/java/TeaVMLauncher.java b/examples/SharedLib/app/teavm/src/main/java/TeaVMLauncher.java index 086a643f..1355899f 100644 --- a/examples/SharedLib/app/teavm/src/main/java/TeaVMLauncher.java +++ b/examples/SharedLib/app/teavm/src/main/java/TeaVMLauncher.java @@ -1,13 +1,13 @@ -import com.github.xpenatan.gdx.backends.teavm.TeaApplication; -import com.github.xpenatan.gdx.backends.teavm.TeaApplicationConfiguration; +import com.github.xpenatan.gdx.teavm.backends.web.WebApplication; +import com.github.xpenatan.gdx.teavm.backends.web.WebApplicationConfiguration; import com.github.xpenatan.jParser.example.app.SharedLibApp; public class TeaVMLauncher { public static void main(String[] args) { - TeaApplicationConfiguration config = new TeaApplicationConfiguration("canvas"); + WebApplicationConfiguration config = new WebApplicationConfiguration("canvas"); config.width = 0; config.height = 0; config.showDownloadLogs = true; - new TeaApplication(new SharedLibApp(), config); + new WebApplication(new SharedLibApp(), config); } } \ No newline at end of file diff --git a/examples/SharedLib/libA/lib-build/build.gradle.kts b/examples/SharedLib/libA/lib-build/build.gradle.kts index 72a311bd..c540427e 100644 --- a/examples/SharedLib/libA/lib-build/build.gradle.kts +++ b/examples/SharedLib/libA/lib-build/build.gradle.kts @@ -57,7 +57,7 @@ tasks.register("LibA_build_project_teavm") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibA_build_project_windows64") { +tasks.register("LibA_build_project_jni_windows64") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -65,7 +65,7 @@ tasks.register("LibA_build_project_windows64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibA_build_project_linux64") { +tasks.register("LibA_build_project_jni_linux64") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -73,7 +73,7 @@ tasks.register("LibA_build_project_linux64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibA_build_project_mac64") { +tasks.register("LibA_build_project_jni_mac64") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -81,7 +81,7 @@ tasks.register("LibA_build_project_mac64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibA_build_project_macArm") { +tasks.register("LibA_build_project_jni_macArm") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -89,7 +89,7 @@ tasks.register("LibA_build_project_macArm") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibA_build_project_android") { +tasks.register("LibA_build_project_jni_android") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -97,7 +97,7 @@ tasks.register("LibA_build_project_android") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibA_build_project_ios") { +tasks.register("LibA_build_project_jni_ios") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) diff --git a/examples/SharedLib/libB/lib-ffm/build.gradle.kts b/examples/SharedLib/libA/lib-desktop-ffm/build.gradle.kts similarity index 60% rename from examples/SharedLib/libB/lib-ffm/build.gradle.kts rename to examples/SharedLib/libA/lib-desktop-ffm/build.gradle.kts index 167fd184..f3ba8791 100644 --- a/examples/SharedLib/libB/lib-ffm/build.gradle.kts +++ b/examples/SharedLib/libA/lib-desktop-ffm/build.gradle.kts @@ -2,30 +2,47 @@ plugins { id("java") id("java-library") } + // FFM (java.lang.foreign.*) requires JDK 22+ at runtime. java { sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) } + tasks.withType { javaCompiler.set(javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(25)) }) options.release.set(null as Int?) } + dependencies { - implementation(project(":examples:SharedLib:libA:lib-ffm")) if(LibExt.exampleUseRepoLibs) { api("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") api("com.github.xpenatan.jParser:idl-core:-SNAPSHOT") - api("com.github.xpenatan.jParser:idl-helper-ffm:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-helper-desktop-ffm:-SNAPSHOT") } else { api(project(":loader:loader-core")) api(project(":idl:idl-core")) - api(project(":idl-helper:idl-helper-ffm")) + api(project(":idl-helper:idl-helper-desktop-ffm")) } } + +// Bundle FFM-compiled native libraries into the JAR. +val libDir = "${projectDir}/../lib-build/build/c++/libs" +val windowsFile = "$libDir/windows/vc/ffm/LibA64.dll" +val linuxFile = "$libDir/linux/ffm/libLibA64.so" +val macFile = "$libDir/mac/ffm/libLibA64.dylib" +val macArmFile = "$libDir/mac/arm/ffm/libLibAarm64.dylib" + +tasks.jar { + from(windowsFile) + from(linuxFile) + from(macFile) + from(macArmFile) +} + tasks.named("clean") { doFirst { val srcPath = "$projectDir/src/main/" diff --git a/examples/SharedLib/libA/lib-desktop/build.gradle.kts b/examples/SharedLib/libA/lib-desktop-jni/build.gradle.kts similarity index 93% rename from examples/SharedLib/libA/lib-desktop/build.gradle.kts rename to examples/SharedLib/libA/lib-desktop-jni/build.gradle.kts index 39fdb553..d4a31aa3 100644 --- a/examples/SharedLib/libA/lib-desktop/build.gradle.kts +++ b/examples/SharedLib/libA/lib-desktop-jni/build.gradle.kts @@ -10,7 +10,7 @@ java { val libDir = "${projectDir}/../lib-build/build/c++/libs" //val windowsFile = "$libDir/windows/LibA64.dll" //val windowsFile = "$libDir/windows/vc/LibA64.dll" -val windowsFile = "$libDir/windows/vc/ffm/LibA64.dll" +val windowsFile = "$libDir/windows/vc/LibA64.dll" val linuxFile = "$libDir/linux/libLibA64.so" val macFile = "$libDir/mac/libLibA64.dylib" val macArmFile = "$libDir/mac/arm/libLibAarm64.dylib" diff --git a/examples/SharedLib/libB/lib-build/build.gradle.kts b/examples/SharedLib/libB/lib-build/build.gradle.kts index 8dcdf9a3..bb885078 100644 --- a/examples/SharedLib/libB/lib-build/build.gradle.kts +++ b/examples/SharedLib/libB/lib-build/build.gradle.kts @@ -59,7 +59,7 @@ tasks.register("LibB_build_project_teavm") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibB_build_project_windows64") { +tasks.register("LibB_build_project_jni_windows64") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -67,7 +67,7 @@ tasks.register("LibB_build_project_windows64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibB_build_project_linux64") { +tasks.register("LibB_build_project_jni_linux64") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -75,7 +75,7 @@ tasks.register("LibB_build_project_linux64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibB_build_project_mac64") { +tasks.register("LibB_build_project_jni_mac64") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -83,7 +83,7 @@ tasks.register("LibB_build_project_mac64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibB_build_project_macArm") { +tasks.register("LibB_build_project_jni_macArm") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -91,7 +91,7 @@ tasks.register("LibB_build_project_macArm") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibB_build_project_android") { +tasks.register("LibB_build_project_jni_android") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) @@ -99,7 +99,7 @@ tasks.register("LibB_build_project_android") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("LibB_build_project_ios") { +tasks.register("LibB_build_project_jni_ios") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) diff --git a/examples/SharedLib/libA/lib-ffm/build.gradle.kts b/examples/SharedLib/libB/lib-desktop-ffm/build.gradle.kts similarity index 57% rename from examples/SharedLib/libA/lib-ffm/build.gradle.kts rename to examples/SharedLib/libB/lib-desktop-ffm/build.gradle.kts index 91ec2327..1f40000c 100644 --- a/examples/SharedLib/libA/lib-ffm/build.gradle.kts +++ b/examples/SharedLib/libB/lib-desktop-ffm/build.gradle.kts @@ -4,11 +4,6 @@ plugins { } // FFM (java.lang.foreign.*) requires JDK 22+ at runtime. -// We compile with JDK 25 to access the FFM API, but set targetCompatibility -// to Java 11 so Gradle's dependency metadata stays compatible with lower-JVM -// consumer modules. The --release flag is cleared so the JDK 25 API is -// available despite the Java 11 bytecode target. -// It is the consumer's responsibility to run the application on JDK 22+. java { sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) @@ -18,28 +13,40 @@ tasks.withType { javaCompiler.set(javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(25)) }) - // Clear --release so JDK 25 APIs (java.lang.foreign) are accessible - // even with -source 11 -target 11 bytecode output. options.release.set(null as Int?) } dependencies { + implementation(project(":examples:SharedLib:libA:lib-desktop-ffm")) if(LibExt.exampleUseRepoLibs) { api("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") api("com.github.xpenatan.jParser:idl-core:-SNAPSHOT") - api("com.github.xpenatan.jParser:idl-helper-ffm:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-helper-desktop-ffm:-SNAPSHOT") } else { api(project(":loader:loader-core")) api(project(":idl:idl-core")) - api(project(":idl-helper:idl-helper-ffm")) + api(project(":idl-helper:idl-helper-desktop-ffm")) } } +// Bundle FFM-compiled native libraries into the JAR. +val libDir = "${projectDir}/../lib-build/build/c++/libs" +val windowsFile = "$libDir/windows/vc/ffm/LibB64.dll" +val linuxFile = "$libDir/linux/ffm/libLibB64.so" +val macFile = "$libDir/mac/ffm/libLibB64.dylib" +val macArmFile = "$libDir/mac/arm/ffm/libLibBarm64.dylib" + +tasks.jar { + from(windowsFile) + from(linuxFile) + from(macFile) + from(macArmFile) +} + tasks.named("clean") { doFirst { val srcPath = "$projectDir/src/main/" project.delete(files(srcPath)) } } - diff --git a/examples/SharedLib/libB/lib-desktop/build.gradle.kts b/examples/SharedLib/libB/lib-desktop-jni/build.gradle.kts similarity index 93% rename from examples/SharedLib/libB/lib-desktop/build.gradle.kts rename to examples/SharedLib/libB/lib-desktop-jni/build.gradle.kts index 6f106278..353bcd94 100644 --- a/examples/SharedLib/libB/lib-desktop/build.gradle.kts +++ b/examples/SharedLib/libB/lib-desktop-jni/build.gradle.kts @@ -10,7 +10,7 @@ java { val libDir = "${projectDir}/../lib-build/build/c++/libs" //val windowsFile = "$libDir/windows/LibB64.dll" //val windowsFile = "$libDir/windows/vc/LibB64.dll" -val windowsFile = "$libDir/windows/vc/ffm/LibB64.dll" +val windowsFile = "$libDir/windows/vc/LibB64.dll" val linuxFile = "$libDir/linux/libLibB64.so" val macFile = "$libDir/mac/libLibB64.dylib" val macArmFile = "$libDir/mac/arm/libLibBarm64.dylib" diff --git a/examples/TestLib/app/desktop/assets/data/badlogic.jpg b/examples/TestLib/app/assets/data/badlogic.jpg similarity index 100% rename from examples/TestLib/app/desktop/assets/data/badlogic.jpg rename to examples/TestLib/app/assets/data/badlogic.jpg diff --git a/examples/TestLib/app/core/build.gradle.kts b/examples/TestLib/app/core/build.gradle.kts index 6038fc91..54bcd086 100644 --- a/examples/TestLib/app/core/build.gradle.kts +++ b/examples/TestLib/app/core/build.gradle.kts @@ -9,11 +9,8 @@ java { dependencies { // compileOnly: app/core compiles against lib-core's API, but does NOT - // propagate it transitively. Each platform module (desktop, android, teavm) - // provides the actual native bridge implementation: - // - lib-core for JNI (desktop/android) - // - lib-ffm for FFM (desktop with Java 22+) - // - lib-teavm for TeaVM (web) + // propagate it transitively. Each platform module (desktop-jni, desktop-ffm, + // android, teavm) provides the actual native bridge implementation. compileOnly(project(":examples:TestLib:lib:lib-core")) implementation("com.badlogicgames.gdx:gdx:${LibExt.gdxVersion}") diff --git a/examples/TestLib/app/desktop/build.gradle.kts b/examples/TestLib/app/desktop-ffm/build.gradle.kts similarity index 50% rename from examples/TestLib/app/desktop/build.gradle.kts rename to examples/TestLib/app/desktop-ffm/build.gradle.kts index 3c40da0e..90fdbb48 100644 --- a/examples/TestLib/app/desktop/build.gradle.kts +++ b/examples/TestLib/app/desktop-ffm/build.gradle.kts @@ -9,35 +9,20 @@ java { targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) } -// Separate configuration for JNI bridge (used by benchmark comparison tasks only). -val jniBridge by configurations.creating { - isTransitive = true -} - dependencies { implementation(project(":examples:TestLib:app:core")) - - // Choose ONE native bridge — lib-core (JNI) or lib-ffm (FFM). - // Both provide identical public APIs; only the internal native bridge differs. - // implementation(project(":examples:TestLib:lib:lib-core")) // JNI (default) - implementation(project(":examples:TestLib:lib:lib-ffm")) // FFM (Java 22+, no JNI overhead) - - implementation(project(":examples:TestLib:lib:lib-desktop")) + implementation(project(":examples:TestLib:lib:lib-desktop-ffm")) implementation("com.badlogicgames.gdx:gdx-platform:${LibExt.gdxVersion}:natives-desktop") implementation("com.badlogicgames.gdx:gdx-backend-lwjgl3:${LibExt.gdxVersion}") - - // JNI bridge for comparison benchmarks (not on default classpath) - jniBridge(project(":examples:TestLib:lib:lib-core")) } tasks.register("TestLib_run_app_desktop") { group = "example-desktop" - description = "Run desktop app" + description = "Run desktop app (FFM)" mainClass.set("com.github.xpenatan.jParser.example.app.Main") classpath = sourceSets["main"].runtimeClasspath - // FFM requires JDK 22+ and native access javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(25)) }) @@ -50,22 +35,10 @@ tasks.register("TestLib_run_app_desktop") { tasks.register("TestLib_run_benchmark_desktop") { group = "example-desktop" - description = "Run desktop app" + description = "Run enum benchmark (FFM)" mainClass.set("com.github.xpenatan.jParser.example.app.BenchmarkMain") classpath = sourceSets["main"].runtimeClasspath - if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { - jvmArgs("-XstartOnFirstThread") - } -} - -tasks.register("TestLib_run_native_benchmark_desktop") { - group = "example-desktop" - description = "Run native bridge benchmark (JNI vs FFM)" - mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkMain") - classpath = sourceSets["main"].runtimeClasspath - - // FFM requires JDK 22+ and native access javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(25)) }) @@ -77,28 +50,22 @@ tasks.register("TestLib_run_native_benchmark_desktop") { } // --------------------------------------------------------------------------- -// JNI vs FFM comparison benchmark tasks +// FFM benchmark tasks // --------------------------------------------------------------------------- -// Build a JNI classpath: take the default runtime classpath, remove lib-ffm -// artifacts, and add lib-core (JNI) from the jniBridge configuration. -// The JNI native DLL directories are prepended so the JNI-compiled DLLs are -// found by the loader *before* the FFM DLLs bundled inside the desktop JARs. -val testLibJniDllDir = file("${projectDir}/../../lib/lib-build/build/c++/libs/windows/vc") -val idlHelperJniDllDir = file("${rootProject.projectDir}/idl-helper/idl-helper-build/build/c++/libs/windows/vc") -val jniClasspath = files(testLibJniDllDir, idlHelperJniDllDir) + sourceSets["main"].runtimeClasspath.filter { file -> - !file.absolutePath.replace('\\', '/').contains("lib-ffm") && - !file.absolutePath.replace('\\', '/').contains("idl-helper-ffm") -} + configurations["jniBridge"] - val benchmarkDir = layout.buildDirectory.dir("benchmark") -tasks.register("TestLib_benchmark_jni") { +tasks.register("TestLib_benchmark_ffm") { group = "example-benchmark" - description = "Run native bridge benchmark with JNI bridge, save CSV report" + description = "Run native bridge benchmark with FFM bridge, save CSV report" mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkMain") - classpath = jniClasspath - systemProperty("benchmark.output", benchmarkDir.get().file("benchmark_jni.csv").asFile.absolutePath) + classpath = sourceSets["main"].runtimeClasspath + systemProperty("benchmark.output", benchmarkDir.get().file("benchmark_ffm.csv").asFile.absolutePath) + + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(25)) + }) + jvmArgs("--enable-native-access=ALL-UNNAMED") if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { jvmArgs("-XstartOnFirstThread") @@ -109,14 +76,13 @@ tasks.register("TestLib_benchmark_jni") { } } -tasks.register("TestLib_benchmark_ffm") { +tasks.register("TestLib_fps_benchmark_ffm") { group = "example-benchmark" - description = "Run native bridge benchmark with FFM bridge, save CSV report" - mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkMain") + description = "Run FPS benchmark with FFM bridge, save CSV report" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkMain") classpath = sourceSets["main"].runtimeClasspath - systemProperty("benchmark.output", benchmarkDir.get().file("benchmark_ffm.csv").asFile.absolutePath) + systemProperty("benchmark.fps.output", benchmarkDir.get().file("fps_benchmark_ffm.csv").asFile.absolutePath) - // FFM requires JDK 22+ and native access javaLauncher.set(javaToolchains.launcherFor { languageVersion.set(JavaLanguageVersion.of(25)) }) @@ -131,80 +97,41 @@ tasks.register("TestLib_benchmark_ffm") { } } +// --------------------------------------------------------------------------- +// JNI vs FFM comparison benchmark tasks +// These depend on JNI benchmark tasks from the desktop-jni module. +// --------------------------------------------------------------------------- + +val jniBenchmarkDir = project(":examples:TestLib:app:desktop-jni").layout.buildDirectory.dir("benchmark") + tasks.register("TestLib_benchmark_compare") { group = "example-benchmark" description = "Run JNI & FFM benchmarks then print a comparison table" - dependsOn("TestLib_benchmark_jni", "TestLib_benchmark_ffm") + dependsOn(":examples:TestLib:app:desktop-jni:TestLib_benchmark_jni", "TestLib_benchmark_ffm") mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkCompare") classpath = sourceSets["main"].runtimeClasspath args( - benchmarkDir.get().file("benchmark_jni.csv").asFile.absolutePath, + jniBenchmarkDir.get().file("benchmark_jni.csv").asFile.absolutePath, benchmarkDir.get().file("benchmark_ffm.csv").asFile.absolutePath, benchmarkDir.get().file("benchmark_compare.txt").asFile.absolutePath ) } -// Ensure JNI runs before FFM when both are requested -tasks.named("TestLib_benchmark_ffm") { mustRunAfter("TestLib_benchmark_jni") } - -// --------------------------------------------------------------------------- -// JNI vs FFM FPS benchmark tasks -// --------------------------------------------------------------------------- - -val fpsBenchmarkDir = layout.buildDirectory.dir("benchmark") - -tasks.register("TestLib_fps_benchmark_jni") { - group = "example-benchmark" - description = "Run FPS benchmark with JNI bridge, save CSV report" - mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkMain") - classpath = jniClasspath - systemProperty("benchmark.fps.output", fpsBenchmarkDir.get().file("fps_benchmark_jni.csv").asFile.absolutePath) - - if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { - jvmArgs("-XstartOnFirstThread") - } - - doFirst { - fpsBenchmarkDir.get().asFile.mkdirs() - } -} - -tasks.register("TestLib_fps_benchmark_ffm") { - group = "example-benchmark" - description = "Run FPS benchmark with FFM bridge, save CSV report" - mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkMain") - classpath = sourceSets["main"].runtimeClasspath - systemProperty("benchmark.fps.output", fpsBenchmarkDir.get().file("fps_benchmark_ffm.csv").asFile.absolutePath) - - javaLauncher.set(javaToolchains.launcherFor { - languageVersion.set(JavaLanguageVersion.of(25)) - }) - jvmArgs("--enable-native-access=ALL-UNNAMED") - - if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { - jvmArgs("-XstartOnFirstThread") - } - - doFirst { - fpsBenchmarkDir.get().asFile.mkdirs() - } -} - tasks.register("TestLib_fps_benchmark_compare") { group = "example-benchmark" description = "Run JNI & FFM FPS benchmarks then print a comparison table" - dependsOn("TestLib_fps_benchmark_jni", "TestLib_fps_benchmark_ffm") + dependsOn(":examples:TestLib:app:desktop-jni:TestLib_fps_benchmark_jni", "TestLib_fps_benchmark_ffm") mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkCompare") classpath = sourceSets["main"].runtimeClasspath args( - fpsBenchmarkDir.get().file("fps_benchmark_jni.csv").asFile.absolutePath, - fpsBenchmarkDir.get().file("fps_benchmark_ffm.csv").asFile.absolutePath, - fpsBenchmarkDir.get().file("fps_benchmark_compare.txt").asFile.absolutePath + jniBenchmarkDir.get().file("fps_benchmark_jni.csv").asFile.absolutePath, + benchmarkDir.get().file("fps_benchmark_ffm.csv").asFile.absolutePath, + benchmarkDir.get().file("fps_benchmark_compare.txt").asFile.absolutePath ) } // Ensure JNI runs before FFM when both are requested -tasks.named("TestLib_fps_benchmark_ffm") { mustRunAfter("TestLib_fps_benchmark_jni") } - +tasks.named("TestLib_benchmark_ffm") { mustRunAfter(":examples:TestLib:app:desktop-jni:TestLib_benchmark_jni") } +tasks.named("TestLib_fps_benchmark_ffm") { mustRunAfter(":examples:TestLib:app:desktop-jni:TestLib_fps_benchmark_jni") } diff --git a/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java b/examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java similarity index 100% rename from examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java rename to examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java diff --git a/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/Main.java b/examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/Main.java similarity index 100% rename from examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/Main.java rename to examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/Main.java diff --git a/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java b/examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java similarity index 100% rename from examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java rename to examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java diff --git a/examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java b/examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java similarity index 100% rename from examples/TestLib/app/desktop/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java rename to examples/TestLib/app/desktop-ffm/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java diff --git a/examples/TestLib/app/desktop-jni/build.gradle.kts b/examples/TestLib/app/desktop-jni/build.gradle.kts new file mode 100644 index 00000000..c89d870a --- /dev/null +++ b/examples/TestLib/app/desktop-jni/build.gradle.kts @@ -0,0 +1,81 @@ +import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform + +plugins { + id("java") +} + +java { + sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) + targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) +} + +dependencies { + implementation(project(":examples:TestLib:app:core")) + implementation(project(":examples:TestLib:lib:lib-core")) + implementation(project(":examples:TestLib:lib:lib-desktop-jni")) + implementation(project(":idl-helper:idl-helper-desktop-jni")) + + implementation("com.badlogicgames.gdx:gdx-platform:${LibExt.gdxVersion}:natives-desktop") + implementation("com.badlogicgames.gdx:gdx-backend-lwjgl3:${LibExt.gdxVersion}") +} + +tasks.register("TestLib_run_app_desktop") { + group = "example-desktop" + description = "Run desktop app (JNI)" + mainClass.set("com.github.xpenatan.jParser.example.app.Main") + classpath = sourceSets["main"].runtimeClasspath + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } +} + +tasks.register("TestLib_run_benchmark_desktop") { + group = "example-desktop" + description = "Run enum benchmark (JNI)" + mainClass.set("com.github.xpenatan.jParser.example.app.BenchmarkMain") + classpath = sourceSets["main"].runtimeClasspath + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } +} + +// --------------------------------------------------------------------------- +// JNI benchmark tasks +// --------------------------------------------------------------------------- + +val benchmarkDir = layout.buildDirectory.dir("benchmark") + +tasks.register("TestLib_benchmark_jni") { + group = "example-benchmark" + description = "Run native bridge benchmark with JNI bridge, save CSV report" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeBenchmarkMain") + classpath = sourceSets["main"].runtimeClasspath + systemProperty("benchmark.output", benchmarkDir.get().file("benchmark_jni.csv").asFile.absolutePath) + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } + + doFirst { + benchmarkDir.get().asFile.mkdirs() + } +} + +tasks.register("TestLib_fps_benchmark_jni") { + group = "example-benchmark" + description = "Run FPS benchmark with JNI bridge, save CSV report" + mainClass.set("com.github.xpenatan.jParser.example.app.NativeBridgeFpsBenchmarkMain") + classpath = sourceSets["main"].runtimeClasspath + systemProperty("benchmark.fps.output", benchmarkDir.get().file("fps_benchmark_jni.csv").asFile.absolutePath) + + if(DefaultNativePlatform.getCurrentOperatingSystem().isMacOsX) { + jvmArgs("-XstartOnFirstThread") + } + + doFirst { + benchmarkDir.get().asFile.mkdirs() + } +} + diff --git a/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java new file mode 100644 index 00000000..b27228fb --- /dev/null +++ b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/BenchmarkMain.java @@ -0,0 +1,12 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; + +public class BenchmarkMain { + + public static void main(String[] args) { + Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration(); + new Lwjgl3Application(new EnunBenchmarkAppTest(), config); + } +} \ No newline at end of file diff --git a/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java new file mode 100644 index 00000000..aba2748d --- /dev/null +++ b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/Main.java @@ -0,0 +1,12 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; + +public class Main { + + public static void main(String[] args) { + Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration(); + new Lwjgl3Application(new AppTest(), config); + } +} \ No newline at end of file diff --git a/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java new file mode 100644 index 00000000..d28d0cc1 --- /dev/null +++ b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeBenchmarkMain.java @@ -0,0 +1,14 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; + +public class NativeBridgeBenchmarkMain { + + public static void main(String[] args) { + Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration(); + config.setTitle("Native Bridge Benchmark"); + new Lwjgl3Application(new NativeBridgeBenchmarkApp(), config); + } +} + diff --git a/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java new file mode 100644 index 00000000..00051aaa --- /dev/null +++ b/examples/TestLib/app/desktop-jni/src/main/java/com/github/xpenatan/jParser/example/app/NativeBridgeFpsBenchmarkMain.java @@ -0,0 +1,16 @@ +package com.github.xpenatan.jParser.example.app; + +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application; +import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration; + +public class NativeBridgeFpsBenchmarkMain { + + public static void main(String[] args) { + Lwjgl3ApplicationConfiguration config = new Lwjgl3ApplicationConfiguration(); + config.setTitle("FPS Benchmark"); + config.useVsync(false); + config.setForegroundFPS(0); + new Lwjgl3Application(new NativeBridgeFpsBenchmarkApp(), config); + } +} + diff --git a/examples/TestLib/app/teavm/build.gradle.kts b/examples/TestLib/app/teavm/build.gradle.kts index d34361e9..a877a3b9 100644 --- a/examples/TestLib/app/teavm/build.gradle.kts +++ b/examples/TestLib/app/teavm/build.gradle.kts @@ -1,12 +1,5 @@ plugins { id("java") - id("org.gretty") version("4.1.10") -} - - -project.extra["webAppDir"] = File(projectDir, "build/dist/webapp") -gretty { - contextPath = "/" } java { @@ -19,37 +12,19 @@ dependencies { implementation(project(":examples:TestLib:lib:lib-teavm")) implementation("com.badlogicgames.gdx:gdx:${LibExt.gdxVersion}") - implementation("com.github.xpenatan.gdx-teavm:backend-teavm:${LibExt.gdxTeaVMVersion}") + implementation("com.github.xpenatan.gdx-teavm:backend-web:${LibExt.gdxTeaVMVersion}") } -tasks.register("TestLib_build_app_teavm") { +tasks.register("TestLib_run_app_teavm") { group = "example-teavm" description = "Build teavm app" mainClass.set("Build") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_run_app_teavm") { - group = "example-teavm" - description = "Run teavm app" - val list = listOf("TestLib_build_app_teavm", "jettyRun") - dependsOn(list) - - tasks.findByName("jettyRun")?.mustRunAfter("TestLib_build_app_teavm") -} - -tasks.register("TestLib_build_benchmark_teavm") { +tasks.register("TestLib_run_benchmark_teavm") { group = "example-teavm" description = "Build teavm benchmark" mainClass.set("BenchmarkBuild") classpath = sourceSets["main"].runtimeClasspath -} - -tasks.register("TestLib_run_benchmark_teavm") { - group = "example-teavm" - description = "Run teavm benchmark" - val list = listOf("TestLib_build_benchmark_teavm", "jettyRun") - dependsOn(list) - - tasks.findByName("jettyRun")?.mustRunAfter("TestLib_build_benchmark_teavm") } \ No newline at end of file diff --git a/examples/TestLib/app/teavm/src/main/java/BenchmarkBuild.java b/examples/TestLib/app/teavm/src/main/java/BenchmarkBuild.java index 058e4554..210b1926 100644 --- a/examples/TestLib/app/teavm/src/main/java/BenchmarkBuild.java +++ b/examples/TestLib/app/teavm/src/main/java/BenchmarkBuild.java @@ -1,24 +1,21 @@ -import com.github.xpenatan.gdx.backends.teavm.config.AssetFileHandle; -import com.github.xpenatan.gdx.backends.teavm.config.TeaBuildConfiguration; -import com.github.xpenatan.gdx.backends.teavm.config.TeaBuilder; +import com.github.xpenatan.gdx.teavm.backends.shared.config.AssetFileHandle; +import com.github.xpenatan.gdx.teavm.backends.shared.config.compiler.TeaCompiler; +import com.github.xpenatan.gdx.teavm.backends.web.config.backend.WebBackend; import java.io.File; import java.io.IOException; -import org.teavm.tooling.TeaVMTargetType; -import org.teavm.tooling.TeaVMTool; import org.teavm.vm.TeaVMOptimizationLevel; public class BenchmarkBuild { public static void main(String[] args) throws IOException { - TeaBuildConfiguration teaBuildConfiguration = new TeaBuildConfiguration(); - teaBuildConfiguration.assetsPath.add(new AssetFileHandle("../desktop/assets")); - teaBuildConfiguration.webappPath = new File("build/dist").getCanonicalPath(); - teaBuildConfiguration.targetType = TeaVMTargetType.WEBASSEMBLY_GC; - TeaBuilder.config(teaBuildConfiguration); - TeaVMTool tool = new TeaVMTool(); - tool.setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED); - tool.setMainClass(BenchmarkLauncher.class.getName()); - tool.setObfuscated(false); - TeaBuilder.build(tool); + AssetFileHandle assetsPath = new AssetFileHandle("../assets"); + WebBackend webBackend = new WebBackend(); + webBackend.setStartJettyAfterBuild(true); + new TeaCompiler(webBackend) + .addAssets(assetsPath) + .setOptimizationLevel(TeaVMOptimizationLevel.ADVANCED) + .setMainClass(BenchmarkLauncher.class.getName()) + .setObfuscated(false) + .build(new File("build/dist")); } } diff --git a/examples/TestLib/app/teavm/src/main/java/BenchmarkLauncher.java b/examples/TestLib/app/teavm/src/main/java/BenchmarkLauncher.java index f76fc6b1..97bb9b98 100644 --- a/examples/TestLib/app/teavm/src/main/java/BenchmarkLauncher.java +++ b/examples/TestLib/app/teavm/src/main/java/BenchmarkLauncher.java @@ -1,13 +1,13 @@ -import com.github.xpenatan.gdx.backends.teavm.TeaApplication; -import com.github.xpenatan.gdx.backends.teavm.TeaApplicationConfiguration; +import com.github.xpenatan.gdx.teavm.backends.web.WebApplication; +import com.github.xpenatan.gdx.teavm.backends.web.WebApplicationConfiguration; import com.github.xpenatan.jParser.example.app.EnunBenchmarkAppTest; public class BenchmarkLauncher { public static void main(String[] args) { - TeaApplicationConfiguration config = new TeaApplicationConfiguration("canvas"); + WebApplicationConfiguration config = new WebApplicationConfiguration("canvas"); config.width = 0; config.height = 0; config.showDownloadLogs = true; - new TeaApplication(new EnunBenchmarkAppTest(), config); + new WebApplication(new EnunBenchmarkAppTest(), config); } } \ No newline at end of file diff --git a/examples/TestLib/app/teavm/src/main/java/Build.java b/examples/TestLib/app/teavm/src/main/java/Build.java index 2b87e183..08c46b84 100644 --- a/examples/TestLib/app/teavm/src/main/java/Build.java +++ b/examples/TestLib/app/teavm/src/main/java/Build.java @@ -1,32 +1,21 @@ -import com.github.xpenatan.gdx.backends.teavm.config.AssetFileHandle; -import com.github.xpenatan.gdx.backends.teavm.config.TeaBuildConfiguration; -import com.github.xpenatan.gdx.backends.teavm.config.TeaBuilder; +import com.github.xpenatan.gdx.teavm.backends.shared.config.AssetFileHandle; +import com.github.xpenatan.gdx.teavm.backends.shared.config.compiler.TeaCompiler; +import com.github.xpenatan.gdx.teavm.backends.web.config.backend.WebBackend; import java.io.File; import java.io.IOException; -import org.teavm.tooling.TeaVMSourceFilePolicy; -import org.teavm.tooling.TeaVMTargetType; -import org.teavm.tooling.TeaVMTool; -import org.teavm.tooling.sources.DirectorySourceFileProvider; import org.teavm.vm.TeaVMOptimizationLevel; public class Build { public static void main(String[] args) throws IOException { - TeaBuildConfiguration teaBuildConfiguration = new TeaBuildConfiguration(); - teaBuildConfiguration.assetsPath.add(new AssetFileHandle("../desktop/assets")); - teaBuildConfiguration.webappPath = new File("build/dist").getCanonicalPath(); - teaBuildConfiguration.targetType = TeaVMTargetType.JAVASCRIPT; - TeaBuilder.config(teaBuildConfiguration); - - TeaVMTool tool = new TeaVMTool(); - tool.setObfuscated(false); - tool.setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE); - tool.setMainClass(TeaVMLauncher.class.getName()); - -// tool.setDebugInformationGenerated(true); -// tool.setSourceMapsFileGenerated(true); -// tool.setSourceFilePolicy(TeaVMSourceFilePolicy.COPY); - - TeaBuilder.build(tool); + AssetFileHandle assetsPath = new AssetFileHandle("../assets"); + WebBackend webBackend = new WebBackend(); + webBackend.setStartJettyAfterBuild(true); + new TeaCompiler(webBackend) + .addAssets(assetsPath) + .setOptimizationLevel(TeaVMOptimizationLevel.SIMPLE) + .setMainClass(TeaVMLauncher.class.getName()) + .setObfuscated(false) + .build(new File("build/dist")); } } diff --git a/examples/TestLib/app/teavm/src/main/java/TeaVMLauncher.java b/examples/TestLib/app/teavm/src/main/java/TeaVMLauncher.java index ebc3ff92..f08de6d4 100644 --- a/examples/TestLib/app/teavm/src/main/java/TeaVMLauncher.java +++ b/examples/TestLib/app/teavm/src/main/java/TeaVMLauncher.java @@ -1,13 +1,13 @@ -import com.github.xpenatan.gdx.backends.teavm.TeaApplication; -import com.github.xpenatan.gdx.backends.teavm.TeaApplicationConfiguration; +import com.github.xpenatan.gdx.teavm.backends.web.WebApplication; +import com.github.xpenatan.gdx.teavm.backends.web.WebApplicationConfiguration; import com.github.xpenatan.jParser.example.app.AppTest; public class TeaVMLauncher { public static void main(String[] args) { - TeaApplicationConfiguration config = new TeaApplicationConfiguration("canvas"); + WebApplicationConfiguration config = new WebApplicationConfiguration("canvas"); config.width = 0; config.height = 0; config.showDownloadLogs = true; - new TeaApplication(new AppTest(), config); + new WebApplication(new AppTest(), config); } } \ No newline at end of file diff --git a/examples/TestLib/lib/lib-build/build.gradle.kts b/examples/TestLib/lib/lib-build/build.gradle.kts index e35a5198..9e166928 100644 --- a/examples/TestLib/lib/lib-build/build.gradle.kts +++ b/examples/TestLib/lib/lib-build/build.gradle.kts @@ -56,92 +56,93 @@ tasks.register("TestLib_build_project_teavm") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_windows64") { +// FFM tasks — generate FFM Java code and/or compile native libs with FFMGlue + +tasks.register("TestLib_build_project_ffm") { group = "lib" - description = "Generate native project" + description = "Generate FFM Java code only (no native compilation)" mainClass.set(mainClassName) - args = mutableListOf("windows64") + args = mutableListOf("ffm") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_linux64") { +tasks.register("TestLib_build_project_ffm_windows64") { group = "lib" - description = "Generate native project" + description = "Generate FFM Java code and compile for Windows with FFMGlue" mainClass.set(mainClassName) - args = mutableListOf("linux64") + args = mutableListOf("ffm", "ffm_windows64") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_mac64") { +tasks.register("TestLib_build_project_ffm_linux64") { group = "lib" - description = "Generate native project" + description = "Generate FFM Java code and compile for Linux with FFMGlue" mainClass.set(mainClassName) - args = mutableListOf("mac64") + args = mutableListOf("ffm", "ffm_linux64") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_macArm") { +tasks.register("TestLib_build_project_ffm_mac64") { group = "lib" - description = "Generate native project" + description = "Generate FFM Java code and compile for Mac with FFMGlue" mainClass.set(mainClassName) - args = mutableListOf("macArm") + args = mutableListOf("ffm", "ffm_mac64") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_android") { +tasks.register("TestLib_build_project_ffm_macArm") { group = "lib" - description = "Generate native project" + description = "Generate FFM Java code and compile for Mac ARM with FFMGlue" mainClass.set(mainClassName) - args = mutableListOf("android") + args = mutableListOf("ffm", "ffm_macArm") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_ios") { +tasks.register("TestLib_build_project_jni_windows64") { group = "lib" description = "Generate native project" mainClass.set(mainClassName) - args = mutableListOf("ios") + args = mutableListOf("windows64") classpath = sourceSets["main"].runtimeClasspath } -// FFM tasks — generate FFM Java code and/or compile native libs with FFMGlue - -tasks.register("TestLib_build_project_ffm") { +tasks.register("TestLib_build_project_jni_linux64") { group = "lib" - description = "Generate FFM Java code only (no native compilation)" + description = "Generate native project" mainClass.set(mainClassName) - args = mutableListOf("ffm") + args = mutableListOf("linux64") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_ffm_windows64") { +tasks.register("TestLib_build_project_jni_mac64") { group = "lib" - description = "Generate FFM Java code and compile for Windows with FFMGlue" + description = "Generate native project" mainClass.set(mainClassName) - args = mutableListOf("ffm", "ffm_windows64") + args = mutableListOf("mac64") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_ffm_linux64") { +tasks.register("TestLib_build_project_jni_macArm") { group = "lib" - description = "Generate FFM Java code and compile for Linux with FFMGlue" + description = "Generate native project" mainClass.set(mainClassName) - args = mutableListOf("ffm", "ffm_linux64") + args = mutableListOf("macArm") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_ffm_mac64") { +tasks.register("TestLib_build_project_jni_android") { group = "lib" - description = "Generate FFM Java code and compile for Mac with FFMGlue" + description = "Generate native project" mainClass.set(mainClassName) - args = mutableListOf("ffm", "ffm_mac64") + args = mutableListOf("android") classpath = sourceSets["main"].runtimeClasspath } -tasks.register("TestLib_build_project_ffm_macArm") { +tasks.register("TestLib_build_project_jni_ios") { group = "lib" - description = "Generate FFM Java code and compile for Mac ARM with FFMGlue" + description = "Generate native project" mainClass.set(mainClassName) - args = mutableListOf("ffm", "ffm_macArm") + args = mutableListOf("ios") classpath = sourceSets["main"].runtimeClasspath } + diff --git a/examples/TestLib/lib/lib-ffm/build.gradle.kts b/examples/TestLib/lib/lib-desktop-ffm/build.gradle.kts similarity index 55% rename from examples/TestLib/lib/lib-ffm/build.gradle.kts rename to examples/TestLib/lib/lib-desktop-ffm/build.gradle.kts index 91ec2327..6bf13b02 100644 --- a/examples/TestLib/lib/lib-ffm/build.gradle.kts +++ b/examples/TestLib/lib/lib-desktop-ffm/build.gradle.kts @@ -4,11 +4,8 @@ plugins { } // FFM (java.lang.foreign.*) requires JDK 22+ at runtime. -// We compile with JDK 25 to access the FFM API, but set targetCompatibility -// to Java 11 so Gradle's dependency metadata stays compatible with lower-JVM -// consumer modules. The --release flag is cleared so the JDK 25 API is -// available despite the Java 11 bytecode target. -// It is the consumer's responsibility to run the application on JDK 22+. +// Compile with JDK 25 for API access, but target Java 8 bytecode +// so Gradle metadata stays compatible with lower-JVM consumers. java { sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) targetCompatibility = JavaVersion.toVersion(LibExt.java8Target) @@ -18,8 +15,6 @@ tasks.withType { javaCompiler.set(javaToolchains.compilerFor { languageVersion.set(JavaLanguageVersion.of(25)) }) - // Clear --release so JDK 25 APIs (java.lang.foreign) are accessible - // even with -source 11 -target 11 bytecode output. options.release.set(null as Int?) } @@ -27,19 +22,32 @@ dependencies { if(LibExt.exampleUseRepoLibs) { api("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") api("com.github.xpenatan.jParser:idl-core:-SNAPSHOT") - api("com.github.xpenatan.jParser:idl-helper-ffm:-SNAPSHOT") + api("com.github.xpenatan.jParser:idl-helper-desktop-ffm:-SNAPSHOT") } else { api(project(":loader:loader-core")) api(project(":idl:idl-core")) - api(project(":idl-helper:idl-helper-ffm")) + api(project(":idl-helper:idl-helper-desktop-ffm")) } } +// Bundle FFM-compiled native libraries into the JAR. +val libDir = "${projectDir}/../lib-build/build/c++/libs" +val windowsFile = "$libDir/windows/vc/ffm/TestLib64.dll" +val linuxFile = "$libDir/linux/ffm/libTestLib64.so" +val macFile = "$libDir/mac/ffm/libTestLib64.dylib" +val macArmFile = "$libDir/mac/arm/ffm/libTestLibarm64.dylib" + +tasks.jar { + from(windowsFile) + from(linuxFile) + from(macFile) + from(macArmFile) +} + tasks.named("clean") { doFirst { val srcPath = "$projectDir/src/main/" project.delete(files(srcPath)) } } - diff --git a/examples/TestLib/lib/lib-desktop/build.gradle.kts b/examples/TestLib/lib/lib-desktop-jni/build.gradle.kts similarity index 90% rename from examples/TestLib/lib/lib-desktop/build.gradle.kts rename to examples/TestLib/lib/lib-desktop-jni/build.gradle.kts index 83946952..13236c87 100644 --- a/examples/TestLib/lib/lib-desktop/build.gradle.kts +++ b/examples/TestLib/lib/lib-desktop-jni/build.gradle.kts @@ -9,7 +9,7 @@ java { val libDir = "${projectDir}/../lib-build/build/c++/libs" //val windowsFile = "$libDir/windows/TestLib64.dll" -val windowsFile = "$libDir/windows/vc/ffm/TestLib64.dll" +val windowsFile = "$libDir/windows/vc/TestLib64.dll" val linuxFile = "$libDir/linux/libTestLib64.so" val macFile = "$libDir/mac/libTestLib64.dylib" val macArmFile = "$libDir/mac/arm/libTestLibarm64.dylib" @@ -23,11 +23,11 @@ tasks.jar { dependencies { if(LibExt.exampleUseRepoLibs) { - implementation("com.github.xpenatan.jParser:idl-helper-desktop:-SNAPSHOT") + implementation("com.github.xpenatan.jParser:idl-helper-desktop-jni:-SNAPSHOT") testImplementation("com.github.xpenatan.jParser:loader-core:-SNAPSHOT") } else { - implementation(project(":idl-helper:idl-helper-desktop")) + implementation(project(":idl-helper:idl-helper-desktop-jni")) testImplementation(project(":loader:loader-core")) } testImplementation(project(":examples:TestLib:lib:lib-core")) diff --git a/examples/TestLib/lib/lib-desktop/src/test/java/com/github/xpenatan/jParser/example/NormalClassTest.java b/examples/TestLib/lib/lib-desktop/src/test/java/com/github/xpenatan/jParser/example/NormalClassTest.java deleted file mode 100644 index 619b3949..00000000 --- a/examples/TestLib/lib/lib-desktop/src/test/java/com/github/xpenatan/jParser/example/NormalClassTest.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.xpenatan.jParser.example; - -import com.github.xpenatan.jParser.example.testlib.NormalClass; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -public class NormalClassTest { - -// @BeforeClass -// public static void setUp() throws Exception { -// String libDir = "build/libs/desktop.jar"; -// new JParserLibraryLoader(libDir).load("exampleLib"); -// } -// -// @Test -// public void test_add_int() { -// NormalClass normalClass = new NormalClass(); -// int ret = normalClass.addIntValue(10, 10); -// assertEquals(20, ret); -// } -// -// @Test -// public void test_static_sub_int() { -// int ret = NormalClass.subIntValue(11, 10); -// assertEquals(1, ret); -// } -// -// @Test -// public void test_static_sub_int_subValue() { -// int ret = NormalClass.subIntValue(11, 10, 1); -// assertEquals(0, ret); -// } -// -// @Test -// public void test_add_float() { -// NormalClass normalClass = new NormalClass(); -// float ret = normalClass.addFloatValue(10.3f, 10.3f); -// assertEquals(20.6, ret, 1.0f); -// } -// -// @Test -// public void test_invert_boolean_should_be_false() { -// NormalClass normalClass = new NormalClass(); -// boolean ret = normalClass.invertBoolean(true); -// assertFalse(ret); -// } -// -// @Test -// public void test_invert_boolean_should_be_true() { -// NormalClass normalClass = new NormalClass(); -// boolean ret = normalClass.invertBoolean(false); -// assertTrue(ret); -// } -// -// @Test -// public void test_attribute() { -// NormalClass normalClass = new NormalClass(); -// normalClass.set_hiddenInt(10); -// int retValue = normalClass.get_hiddenInt(); -// assertEquals(10, retValue); -// } - -} - diff --git a/idl-helper/idl-helper-build/build.gradle.kts b/idl-helper/idl-helper-build/build.gradle.kts index b86d2e3f..36c9ec1c 100644 --- a/idl-helper/idl-helper-build/build.gradle.kts +++ b/idl-helper/idl-helper-build/build.gradle.kts @@ -44,7 +44,7 @@ tasks.register("idl_helper_build_project_teavm") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("idl_helper_build_project_windows64") { +tasks.register("idl_helper_build_project_jni_windows64") { group = "idl-helper" description = "Generate native project" mainClass.set(mainClassName) @@ -52,7 +52,7 @@ tasks.register("idl_helper_build_project_windows64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("idl_helper_build_project_linux64") { +tasks.register("idl_helper_build_project_jni_linux64") { group = "idl-helper" description = "Generate native project" mainClass.set(mainClassName) @@ -60,7 +60,7 @@ tasks.register("idl_helper_build_project_linux64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("idl_helper_build_project_mac64") { +tasks.register("idl_helper_build_project_jni_mac64") { group = "idl-helper" description = "Generate native project" mainClass.set(mainClassName) @@ -68,7 +68,7 @@ tasks.register("idl_helper_build_project_mac64") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("idl_helper_build_project_macArm") { +tasks.register("idl_helper_build_project_jni_macArm") { group = "idl-helper" description = "Generate native project" mainClass.set(mainClassName) @@ -76,7 +76,7 @@ tasks.register("idl_helper_build_project_macArm") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("idl_helper_build_project_android") { +tasks.register("idl_helper_build_project_jni_android") { group = "idl-helper" description = "Generate native project" mainClass.set(mainClassName) @@ -84,7 +84,7 @@ tasks.register("idl_helper_build_project_android") { classpath = sourceSets["main"].runtimeClasspath } -tasks.register("idl_helper_build_project_ios") { +tasks.register("idl_helper_build_project_jni_ios") { group = "idl-helper" description = "Generate native project" mainClass.set(mainClassName) diff --git a/idl-helper/idl-helper-ffm/build.gradle.kts b/idl-helper/idl-helper-desktop-ffm/build.gradle.kts similarity index 70% rename from idl-helper/idl-helper-ffm/build.gradle.kts rename to idl-helper/idl-helper-desktop-ffm/build.gradle.kts index acbac62a..0cf2f87e 100644 --- a/idl-helper/idl-helper-ffm/build.gradle.kts +++ b/idl-helper/idl-helper-desktop-ffm/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("java") } -val moduleName = "idl-helper-ffm" +val moduleName = "idl-helper-desktop-ffm" dependencies { implementation(project(":idl:idl-core")) @@ -24,6 +24,20 @@ tasks.withType { options.release.set(null as Int?) } +// Bundle FFM-compiled native libraries into the JAR. +val libDir = "${projectDir}/../idl-helper-build/build/c++/libs" +val windowsFile = "$libDir/windows/vc/ffm/idl64.dll" +val linuxFile = "$libDir/linux/ffm/libidl64.so" +val macFile = "$libDir/mac/ffm/libidl64.dylib" +val macArmFile = "$libDir/mac/arm/ffm/libidlarm64.dylib" + +tasks.jar { + from(windowsFile) + from(linuxFile) + from(macFile) + from(macArmFile) +} + java { withJavadocJar() withSourcesJar() @@ -46,4 +60,3 @@ publishing { } } } - diff --git a/idl-helper/idl-helper-desktop/build.gradle.kts b/idl-helper/idl-helper-desktop-jni/build.gradle.kts similarity index 90% rename from idl-helper/idl-helper-desktop/build.gradle.kts rename to idl-helper/idl-helper-desktop-jni/build.gradle.kts index 7d62183c..7ecc9df8 100644 --- a/idl-helper/idl-helper-desktop/build.gradle.kts +++ b/idl-helper/idl-helper-desktop-jni/build.gradle.kts @@ -2,7 +2,7 @@ plugins { id("java") } -val moduleName = "idl-helper-desktop" +val moduleName = "idl-helper-desktop-jni" java { sourceCompatibility = JavaVersion.toVersion(LibExt.java8Target) @@ -10,7 +10,7 @@ java { } val libDir = "${projectDir}/../idl-helper-build/build/c++/libs" -val windowsFile = "$libDir/windows/vc/ffm/idl64.dll" +val windowsFile = "$libDir/windows/vc/idl64.dll" val linuxFile = "$libDir/linux/libidl64.so" val macFile = "$libDir/mac/libidl64.dylib" val macArmFile = "$libDir/mac/arm/libidlarm64.dylib" diff --git a/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java b/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java index 6e95d530..695c78cf 100644 --- a/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java +++ b/jParser/jParser-build/src/main/java/com/github/xpenatan/jParser/builder/tool/BuildToolOptions.java @@ -77,7 +77,7 @@ private void setup() { moduleBuildPath = modulePath + "/" + modulePrefix + "-build"; moduleCorePath = modulePath + "/" + modulePrefix + "-core"; moduleTeavmPath = modulePath + "/" + modulePrefix + "-teavm"; - moduleFFMPath = modulePath + "/" + modulePrefix + "-ffm"; + moduleFFMPath = modulePath + "/" + modulePrefix + "-desktop-ffm"; moduleBaseJavaDir = moduleBasePath + "/src/main/java"; cppPath = moduleBuildPath + "/src/main/cpp/"; diff --git a/settings.gradle.kts b/settings.gradle.kts index 114a3ac0..12c1aa8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,8 +14,8 @@ include(":idl-helper:idl-helper-base") include(":idl-helper:idl-helper-build") include(":idl-helper:idl-helper-core") include(":idl-helper:idl-helper-teavm") -include(":idl-helper:idl-helper-desktop") -include(":idl-helper:idl-helper-ffm") +include(":idl-helper:idl-helper-desktop-jni") +include(":idl-helper:idl-helper-desktop-ffm") include(":idl-helper:idl-helper-android") include(":loader:loader-core") @@ -24,34 +24,36 @@ include(":loader:loader-teavm") include(":examples:TestLib:lib:lib-build") include(":examples:TestLib:lib:lib-base") include(":examples:TestLib:lib:lib-core") -include(":examples:TestLib:lib:lib-ffm") -include(":examples:TestLib:lib:lib-desktop") +include(":examples:TestLib:lib:lib-desktop-jni") +include(":examples:TestLib:lib:lib-desktop-ffm") include(":examples:TestLib:lib:lib-teavm") include(":examples:TestLib:lib:lib-android") include(":examples:TestLib:app:core") -include(":examples:TestLib:app:desktop") +include(":examples:TestLib:app:desktop-jni") +include(":examples:TestLib:app:desktop-ffm") include(":examples:TestLib:app:teavm") include(":examples:TestLib:app:android") include(":examples:SharedLib:libA:lib-build") include(":examples:SharedLib:libA:lib-base") include(":examples:SharedLib:libA:lib-core") -include(":examples:SharedLib:libA:lib-ffm") -include(":examples:SharedLib:libA:lib-desktop") +include(":examples:SharedLib:libA:lib-desktop-jni") +include(":examples:SharedLib:libA:lib-desktop-ffm") include(":examples:SharedLib:libA:lib-teavm") include(":examples:SharedLib:libA:lib-android") include(":examples:SharedLib:libB:lib-build") include(":examples:SharedLib:libB:lib-base") include(":examples:SharedLib:libB:lib-core") -include(":examples:SharedLib:libB:lib-ffm") -include(":examples:SharedLib:libB:lib-desktop") +include(":examples:SharedLib:libB:lib-desktop-jni") +include(":examples:SharedLib:libB:lib-desktop-ffm") include(":examples:SharedLib:libB:lib-teavm") include(":examples:SharedLib:libB:lib-android") include(":examples:SharedLib:app:core") -include(":examples:SharedLib:app:desktop") +include(":examples:SharedLib:app:desktop-jni") +include(":examples:SharedLib:app:desktop-ffm") include(":examples:SharedLib:app:teavm") include(":examples:SharedLib:app:android") From 0a36511d4915e9539cada5b31a42312721fc1e3e Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 24 Mar 2026 23:01:27 -0300 Subject: [PATCH 09/12] Fix android shared lib --- examples/SharedLib/app/android/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/SharedLib/app/android/build.gradle.kts b/examples/SharedLib/app/android/build.gradle.kts index a2af8456..ac5ffbd5 100644 --- a/examples/SharedLib/app/android/build.gradle.kts +++ b/examples/SharedLib/app/android/build.gradle.kts @@ -43,7 +43,9 @@ dependencies { natives("com.badlogicgames.gdx:gdx-platform:${LibExt.gdxVersion}:natives-x86") implementation(project(":examples:SharedLib:app:core")) + api(project(":examples:SharedLib:libA:lib-core")) api(project(":examples:SharedLib:libA:lib-android")) + api(project(":examples:SharedLib:libB:lib-core")) api(project(":examples:SharedLib:libB:lib-android")) api(project(":idl-helper:idl-helper-android")) } From eee5dc9e2da25c7fb45ddb6c857082309fbf0637 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 24 Mar 2026 23:07:36 -0300 Subject: [PATCH 10/12] Fix shared lib teavm build --- examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java b/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java index fc9c2471..bfe160da 100644 --- a/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java +++ b/examples/SharedLib/libB/lib-build/src/main/java/BuildLibB.java @@ -302,7 +302,6 @@ private static BuildMultiTarget getTeavmTarget(BuildToolOptions op, IDLReader id linkTarget.headerDirs.add("-include" + op.getCustomSourceDir() + "LibBCustomCode.h"); linkTarget.linkerFlags.add("-Wl,--whole-archive"); linkTarget.linkerFlags.add(libBuildCPPPath + "/libs/emscripten/" + op.libName + "_.a"); - linkTarget.linkerFlags.add(libALibPath + "/LibA_.a"); linkTarget.linkerFlags.add("-Wl,--no-whole-archive"); linkTarget.mainModuleName = "idl"; linkTarget.linkerFlags.add("-sSIDE_MODULE=2"); From c9ecfd340065917a27b03ba3eb05bde952b93f73 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 24 Mar 2026 23:08:49 -0300 Subject: [PATCH 11/12] Remove build files --- .run/LibA_build_project_android.run.xml | 24 ----------------- .run/LibA_build_project_linux64.run.xml | 27 ------------------- .run/LibA_build_project_mac64.run.xml | 27 ------------------- .run/LibA_build_project_macArm.run.xml | 27 ------------------- .run/LibA_build_project_teavm.run.xml | 24 ----------------- .run/LibA_build_project_windows64.run.xml | 24 ----------------- .run/LibB_build_project_android.run.xml | 24 ----------------- .run/LibB_build_project_linux64.run.xml | 27 ------------------- .run/LibB_build_project_mac64.run.xml | 27 ------------------- .run/LibB_build_project_macArm.run.xml | 27 ------------------- .run/LibB_build_project_teavm.run.xml | 24 ----------------- .run/LibB_build_project_windows64.run.xml | 24 ----------------- .run/SharedLib_run_app_desktop.run.xml | 24 ----------------- .run/SharedLib_run_app_teavm.run.xml | 24 ----------------- .run/TestLib_build_project.run.xml | 24 ----------------- .run/TestLib_build_project_all.run.xml | 24 ----------------- .run/TestLib_build_project_android.run.xml | 24 ----------------- .run/TestLib_build_project_linux64.run.xml | 27 ------------------- .run/TestLib_build_project_mac64.run.xml | 27 ------------------- .run/TestLib_build_project_macArm.run.xml | 27 ------------------- .run/TestLib_build_project_teavm.run.xml | 24 ----------------- .run/TestLib_build_project_windows64.run.xml | 24 ----------------- .run/TestLib_run_app_desktop.run.xml | 24 ----------------- .run/TestLib_run_app_teavm.run.xml | 24 ----------------- .run/TestLib_run_benchmark_desktop.run.xml | 24 ----------------- .run/TestLib_run_benchmark_teavm.run.xml | 24 ----------------- .run/idl_helper_build_project_android.run.xml | 24 ----------------- .run/idl_helper_build_project_linux64.run.xml | 27 ------------------- .run/idl_helper_build_project_mac64.run.xml | 27 ------------------- .run/idl_helper_build_project_macArm.run.xml | 27 ------------------- .run/idl_helper_build_project_teavm.run.xml | 24 ----------------- ...idl_helper_build_project_windows64.run.xml | 24 ----------------- 32 files changed, 804 deletions(-) delete mode 100644 .run/LibA_build_project_android.run.xml delete mode 100644 .run/LibA_build_project_linux64.run.xml delete mode 100644 .run/LibA_build_project_mac64.run.xml delete mode 100644 .run/LibA_build_project_macArm.run.xml delete mode 100644 .run/LibA_build_project_teavm.run.xml delete mode 100644 .run/LibA_build_project_windows64.run.xml delete mode 100644 .run/LibB_build_project_android.run.xml delete mode 100644 .run/LibB_build_project_linux64.run.xml delete mode 100644 .run/LibB_build_project_mac64.run.xml delete mode 100644 .run/LibB_build_project_macArm.run.xml delete mode 100644 .run/LibB_build_project_teavm.run.xml delete mode 100644 .run/LibB_build_project_windows64.run.xml delete mode 100644 .run/SharedLib_run_app_desktop.run.xml delete mode 100644 .run/SharedLib_run_app_teavm.run.xml delete mode 100644 .run/TestLib_build_project.run.xml delete mode 100644 .run/TestLib_build_project_all.run.xml delete mode 100644 .run/TestLib_build_project_android.run.xml delete mode 100644 .run/TestLib_build_project_linux64.run.xml delete mode 100644 .run/TestLib_build_project_mac64.run.xml delete mode 100644 .run/TestLib_build_project_macArm.run.xml delete mode 100644 .run/TestLib_build_project_teavm.run.xml delete mode 100644 .run/TestLib_build_project_windows64.run.xml delete mode 100644 .run/TestLib_run_app_desktop.run.xml delete mode 100644 .run/TestLib_run_app_teavm.run.xml delete mode 100644 .run/TestLib_run_benchmark_desktop.run.xml delete mode 100644 .run/TestLib_run_benchmark_teavm.run.xml delete mode 100644 .run/idl_helper_build_project_android.run.xml delete mode 100644 .run/idl_helper_build_project_linux64.run.xml delete mode 100644 .run/idl_helper_build_project_mac64.run.xml delete mode 100644 .run/idl_helper_build_project_macArm.run.xml delete mode 100644 .run/idl_helper_build_project_teavm.run.xml delete mode 100644 .run/idl_helper_build_project_windows64.run.xml diff --git a/.run/LibA_build_project_android.run.xml b/.run/LibA_build_project_android.run.xml deleted file mode 100644 index 85112d31..00000000 --- a/.run/LibA_build_project_android.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/LibA_build_project_linux64.run.xml b/.run/LibA_build_project_linux64.run.xml deleted file mode 100644 index c25a15ca..00000000 --- a/.run/LibA_build_project_linux64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/LibA_build_project_mac64.run.xml b/.run/LibA_build_project_mac64.run.xml deleted file mode 100644 index a26f8244..00000000 --- a/.run/LibA_build_project_mac64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/LibA_build_project_macArm.run.xml b/.run/LibA_build_project_macArm.run.xml deleted file mode 100644 index ed9c455e..00000000 --- a/.run/LibA_build_project_macArm.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/LibA_build_project_teavm.run.xml b/.run/LibA_build_project_teavm.run.xml deleted file mode 100644 index 70e435e9..00000000 --- a/.run/LibA_build_project_teavm.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/LibA_build_project_windows64.run.xml b/.run/LibA_build_project_windows64.run.xml deleted file mode 100644 index 87adfae8..00000000 --- a/.run/LibA_build_project_windows64.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/LibB_build_project_android.run.xml b/.run/LibB_build_project_android.run.xml deleted file mode 100644 index a7a5d88e..00000000 --- a/.run/LibB_build_project_android.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/LibB_build_project_linux64.run.xml b/.run/LibB_build_project_linux64.run.xml deleted file mode 100644 index 1ecff02a..00000000 --- a/.run/LibB_build_project_linux64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/LibB_build_project_mac64.run.xml b/.run/LibB_build_project_mac64.run.xml deleted file mode 100644 index f9ae1454..00000000 --- a/.run/LibB_build_project_mac64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/LibB_build_project_macArm.run.xml b/.run/LibB_build_project_macArm.run.xml deleted file mode 100644 index 8d908a64..00000000 --- a/.run/LibB_build_project_macArm.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/LibB_build_project_teavm.run.xml b/.run/LibB_build_project_teavm.run.xml deleted file mode 100644 index b55adb25..00000000 --- a/.run/LibB_build_project_teavm.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/LibB_build_project_windows64.run.xml b/.run/LibB_build_project_windows64.run.xml deleted file mode 100644 index 985cbb5f..00000000 --- a/.run/LibB_build_project_windows64.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/SharedLib_run_app_desktop.run.xml b/.run/SharedLib_run_app_desktop.run.xml deleted file mode 100644 index 86d16a4c..00000000 --- a/.run/SharedLib_run_app_desktop.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/SharedLib_run_app_teavm.run.xml b/.run/SharedLib_run_app_teavm.run.xml deleted file mode 100644 index 978c4ef1..00000000 --- a/.run/SharedLib_run_app_teavm.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project.run.xml b/.run/TestLib_build_project.run.xml deleted file mode 100644 index e080f31c..00000000 --- a/.run/TestLib_build_project.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project_all.run.xml b/.run/TestLib_build_project_all.run.xml deleted file mode 100644 index ab9e25c0..00000000 --- a/.run/TestLib_build_project_all.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project_android.run.xml b/.run/TestLib_build_project_android.run.xml deleted file mode 100644 index 2c0e1684..00000000 --- a/.run/TestLib_build_project_android.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project_linux64.run.xml b/.run/TestLib_build_project_linux64.run.xml deleted file mode 100644 index a91e46b9..00000000 --- a/.run/TestLib_build_project_linux64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project_mac64.run.xml b/.run/TestLib_build_project_mac64.run.xml deleted file mode 100644 index a0340b03..00000000 --- a/.run/TestLib_build_project_mac64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project_macArm.run.xml b/.run/TestLib_build_project_macArm.run.xml deleted file mode 100644 index 54520984..00000000 --- a/.run/TestLib_build_project_macArm.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project_teavm.run.xml b/.run/TestLib_build_project_teavm.run.xml deleted file mode 100644 index f5a96be2..00000000 --- a/.run/TestLib_build_project_teavm.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_build_project_windows64.run.xml b/.run/TestLib_build_project_windows64.run.xml deleted file mode 100644 index f070ff5c..00000000 --- a/.run/TestLib_build_project_windows64.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_run_app_desktop.run.xml b/.run/TestLib_run_app_desktop.run.xml deleted file mode 100644 index a5a2c447..00000000 --- a/.run/TestLib_run_app_desktop.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_run_app_teavm.run.xml b/.run/TestLib_run_app_teavm.run.xml deleted file mode 100644 index a366253e..00000000 --- a/.run/TestLib_run_app_teavm.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_run_benchmark_desktop.run.xml b/.run/TestLib_run_benchmark_desktop.run.xml deleted file mode 100644 index 713b2cec..00000000 --- a/.run/TestLib_run_benchmark_desktop.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/TestLib_run_benchmark_teavm.run.xml b/.run/TestLib_run_benchmark_teavm.run.xml deleted file mode 100644 index a2b45a52..00000000 --- a/.run/TestLib_run_benchmark_teavm.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/idl_helper_build_project_android.run.xml b/.run/idl_helper_build_project_android.run.xml deleted file mode 100644 index 72d40b06..00000000 --- a/.run/idl_helper_build_project_android.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/idl_helper_build_project_linux64.run.xml b/.run/idl_helper_build_project_linux64.run.xml deleted file mode 100644 index dbfe1b66..00000000 --- a/.run/idl_helper_build_project_linux64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/idl_helper_build_project_mac64.run.xml b/.run/idl_helper_build_project_mac64.run.xml deleted file mode 100644 index cad8c216..00000000 --- a/.run/idl_helper_build_project_mac64.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/idl_helper_build_project_macArm.run.xml b/.run/idl_helper_build_project_macArm.run.xml deleted file mode 100644 index 3ea5a4bd..00000000 --- a/.run/idl_helper_build_project_macArm.run.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - true - true - false - false - false - false - false - - - \ No newline at end of file diff --git a/.run/idl_helper_build_project_teavm.run.xml b/.run/idl_helper_build_project_teavm.run.xml deleted file mode 100644 index 73e640d1..00000000 --- a/.run/idl_helper_build_project_teavm.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file diff --git a/.run/idl_helper_build_project_windows64.run.xml b/.run/idl_helper_build_project_windows64.run.xml deleted file mode 100644 index 974ef0d3..00000000 --- a/.run/idl_helper_build_project_windows64.run.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - true - true - false - false - - - \ No newline at end of file From 79ebbc7d846e62e7226be0ae219ef2c60c8e58f3 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 24 Mar 2026 23:20:20 -0300 Subject: [PATCH 12/12] Update Readme and Agents --- AGENTS.md | 40 ++++++--- README.md | 247 +++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 226 insertions(+), 61 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ce696e72..72f71875 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Project Overview -jParser is a Java code-generation and C/C++ compilation library that bridges native code to JVM platforms (desktop, mobile, web). It reads Java source files containing embedded native code blocks, then generates platform-specific Java source for **JNI** (desktop/mobile) and **TeaVM** (web/WASM) targets. It also supports **WebIDL**-driven automatic binding generation. +jParser is a Java code-generation and C/C++ compilation library that bridges native code to JVM platforms (desktop, mobile, web). It reads Java source files containing embedded native code blocks, then generates platform-specific Java source for **JNI** (desktop/mobile), **FFM** (desktop, Java 22+), and **TeaVM** (web/WASM) targets. It also supports **WebIDL**-driven automatic binding generation. ### Context Resumption & State Persistence @@ -25,15 +25,16 @@ This ensures that if the session is interrupted, the next agent has a perfect "s ### Core Pipeline (`BuilderTool.build()` in `jParser/jParser-build-tool`) 1. **IDL Parsing** — `IDLReader` reads `.idl` files from `lib-build/src/main/cpp/` -2. **Code Generation (JNI)** — `CppCodeParser` (extends `IDLDefaultCodeParser`) reads `lib-base/src/main/java` source, generates JNI Java into `lib-core/src/main/java` -3. **Code Generation (TeaVM)** — `TeaVMCodeParser` generates TeaVM/JS Java into `lib-teavm/src/main/java` -4. **Native Compilation** — `JBuilder.build()` compiles C/C++ for each platform target via `BuildMultiTarget` +2. **Code Generation (JNI)** — `CppCodeParser` (extends `IDLDefaultCodeParser`) reads `lib-base/src/main/java` source, generates JNI Java into `lib-core/src/main/java`. Controlled by `BuildToolOptions.generateCPP` (default: `true`). +3. **Code Generation (TeaVM)** — `TeaVMCodeParser` generates TeaVM/JS Java into `lib-teavm/src/main/java`. Controlled by `BuildToolOptions.generateTeaVM` (default: `true`). +4. **Code Generation (FFM)** — `FFMCodeParser` generates FFM Java (using `java.lang.foreign` MethodHandle downcalls) into `lib-desktop-ffm/src/main/java`. Controlled by `BuildToolOptions.generateFFM` (default: `false`). +5. **Native Compilation** — `JBuilder.build()` compiles C/C++ for each platform target via `BuildMultiTarget` ### Module Layout (follows a strict `-base/-build/-core/-teavm` convention) | Suffix | Purpose | Java target | |---|---|---| -| `lib-base` | Hand-written Java source with embedded `/*[-JNI;-NATIVE]*/` and `/*[-TEAVM;-REPLACE]*/` code blocks | Java 8 | +| `lib-base` | Hand-written Java source with embedded `/*[-JNI;-NATIVE]*/`, `/*[-FFM;-NATIVE]*/`, and `/*[-TEAVM;-REPLACE]*/` code blocks | Java 8 | | `lib-build` | `BuildLib.main()` entry point — configures IDL, targets, runs generation + compilation | Java 11 | | `lib-core` | **Generated** JNI Java output (do not hand-edit) | Java 11 | | `lib-teavm` | **Generated** TeaVM Java output (do not hand-edit) | Java 11 | @@ -46,20 +47,32 @@ This pattern repeats across `jParser/`, `idl-helper/`, `loader/`, and `examples/ ### Key Modules - **`jParser/jParser-core`** — `JParser.generate()` entry point; uses JavaParser to parse/transform Java ASTs. `CodeParser` interface → `DefaultCodeParser` → `IDLDefaultCodeParser`. -- **`jParser/jParser-cpp`** — `CppCodeParser` (header `"JNI"`) generates JNI glue code + `NativeCPPGenerator` emits C++ `.cpp` files. +- **`jParser/jParser-cpp`** — `CppCodeParser` (header `"JNI"`) generates JNI glue code + `NativeCPPGenerator` emits C++ `.cpp` files with JNI calling convention (`jlong`, `jint`, etc.). +- **`jParser/jParser-ffm`** — `FFMCodeParser` (header `"FFM"`) generates Java classes using `java.lang.foreign` MethodHandle downcalls instead of JNI native methods. `FFMCppGenerator` emits C++ `.cpp` files with `extern "C"` and standard C types (`int64_t`, `int32_t`, etc.). Also includes `FFMMethodHandleRegistry`, `FFMNativeCodeGenerator`, and `FFMTypeMapper`. - **`jParser/jParser-teavm`** — `TeaVMCodeParser` (header `"TEAVM"`) generates `@JSBody`-annotated methods for TeaVM. - **`jParser/jParser-idl`** — IDL file parser, class model (`IDLClass`, `IDLMethod`, `IDLAttribute`), and code generation parsers. -- **`jParser/jParser-build`** — `JBuilder`, `BuildConfig`, platform targets (`EmscriptenTarget`, `WindowsMSVCTarget`, `LinuxTarget`, etc.). +- **`jParser/jParser-build`** — `JBuilder`, `BuildConfig`, `BuildToolOptions`, platform targets (`EmscriptenTarget`, `WindowsMSVCTarget`, `WindowsTarget`, `LinuxTarget`, `MacTarget`, `AndroidTarget`, `IOSTarget`). +- **`jParser/jParser-build-tool`** — `BuilderTool.build()` orchestrates the full pipeline: IDL parsing → JNI/TeaVM/FFM code generation → native compilation. +- **`jParser/jParser-base`** — Shared base classes used by all targets (e.g., `IDLUtils`, `IDLString`, `IDLArray`). - **`idl/idl-core`** — `IDLBase` parent class for all native objects (memory management, ownership, dispose). +- **`idl/idl-teavm`** — TeaVM-specific IDL runtime support. +- **`loader/loader-core`** — `JParserLibraryLoader` handles native library loading for desktop and mobile platforms. +- **`loader/loader-teavm`** — TeaVM-specific library loader (asynchronous JS/WASM script loading). ## Code Block Convention -In `lib-base` Java source, native code is embedded via block comments with headers: +In `lib-base` Java source, native code is embedded via block comments with headers. Each target uses its own header prefix. A single source file can contain blocks for all three targets: + ```java /*[-JNI;-NATIVE] MyType* obj = (MyType*)this_addr; obj->doSomething(); */ + +/*[-FFM;-NATIVE] + MyType* obj = (MyType*)this_addr; + obj->doSomething(); +*/ private static native void internal_native_doSomething(long this_addr); /*[-TEAVM;-REPLACE] @@ -67,7 +80,12 @@ private static native void internal_native_doSomething(long this_addr); private static native void internal_native_doSomething(int this_addr); */ ``` -Commands: `-ADD`, `-ADD_RAW`, `-REMOVE`, `-REPLACE`, `-REPLACE_BLOCK`, `-NATIVE`. Use `-IDL_SKIP` on a class comment to prevent IDL generation for that class. + +**Headers**: `JNI`, `FFM`, `TEAVM`. + +**Commands**: `-ADD`, `-ADD_RAW`, `-REMOVE`, `-REPLACE`, `-REPLACE_BLOCK`, `-NATIVE`. Use `-IDL_SKIP` on a class comment to prevent IDL generation for that class. + +**How it works**: `DefaultCodeParser` matches the header prefix (e.g., `JNI`, `FFM`, `TEAVM`) in each block comment. Blocks whose header does not match the active parser are automatically removed from the generated output. The `-NATIVE` command associates C/C++ code with the following `native` method declaration. ## Requirements @@ -214,6 +232,6 @@ Measures how native bridge overhead affects frame rate. Each frame executes a fi - **Generated code is not hand-edited**: `lib-core/`, `lib-teavm/`, and `lib-desktop-ffm/` directories contain generated output with a "Do not make changes" header. - **IDL files** live at `lib-build/src/main/cpp/.idl`. Custom C++ glue code goes in `lib-build/src/main/cpp/custom/`. - **IDLBase** is the parent of all native-bound classes. Memory must be manually managed via `dispose()`. Use `ClassName.NULL` instead of Java `null` for native parameters. -- **Dependencies**: JavaParser (`3.26.1`) for AST manipulation, TeaVM (`0.13.0`) for web target, JUnit 4 for tests. +- **Dependencies**: JavaParser (`3.26.1`) for AST manipulation, TeaVM (`0.13.1`) for web target, JUnit 4 for tests. - **Native bridge selection**: Each example has separate `app/desktop-jni` and `app/desktop-ffm` modules. JNI uses `lib-core` + `lib-desktop-jni`, FFM uses `lib-desktop-ffm`. - +- **JNI vs FFM C++ differences**: JNI glue uses JNI types (`jlong`, `jint`, `JNIEnv*`). FFM glue uses `extern "C"` with standard C types (`int64_t`, `int32_t`) and no JNI environment — calls go through `java.lang.foreign` MethodHandle downcalls. diff --git a/README.md b/README.md index f719d911..d81ec447 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,214 @@ -# jParser +

+

jParser

+

+ A Java code-generation library that bridges C/C++ native code to JVM platforms — desktop, mobile, and web. +

+

-![Build](https://github.com/xpenatan/jParser/actions/workflows/snapshot.yml/badge.svg) -[![Maven Central Version](https://img.shields.io/maven-central/v/com.github.xpenatan.jParser/jParser-core)](https://central.sonatype.com/artifact/com.github.xpenatan.jParser/jParser-core) -[![Static Badge](https://img.shields.io/badge/snapshot---SNAPSHOT-red)](https://central.sonatype.com/service/rest/repository/browse/maven-snapshots/com/github/xpenatan/jParser/) +

+ Build + Maven Central Version + Snapshot + License +

+--- + +## Table of Contents + +- [Overview](#overview) +- [How It Works](#how-it-works) +- [Supported Targets](#supported-targets) +- [Code Block Convention](#code-block-convention) +- [WebIDL Bindings](#webidl-bindings) +- [IDLBase API](#idlbase-api) +- [Requirements](#requirements) +- [Getting Started](#getting-started) +- [Libraries Using jParser](#libraries-using-jparser) +- [License](#license) + +--- -jParser is a compact Java library designed to facilitate the integration of C/C++ code with desktop, mobile, and web platforms, enabling inline writing within Java source code. +## Overview -Inspired by [gdx-jnigen](https://github.com/libgdx/gdx-jnigen), jParser allows you to embed native code within a code block. This block is then translated into the appropriate target-specific code. You can define multiple code block targets within the same Java source file, and for each target, jParser generates a corresponding Java source file. +Inspired by [gdx-jnigen](https://github.com/libgdx/gdx-jnigen), jParser lets you embed native C/C++ code directly inside Java source files using annotated comment blocks. Each block is translated into target-specific Java source code, enabling a single `lib-base` module to produce separate outputs for **JNI** (desktop & mobile), **FFM** (desktop, Java 22+), and **TeaVM** (web via JS/WASM). -For web applications, jParser requires Emscripten to produce JS/WASM files and utilizes [TeaVM](https://github.com/konsoletyper/teavm). The classes generated in the TeaVM module use `JSBody` annotation solution to interact with JavaScript. +For web targets, jParser uses [Emscripten](https://emscripten.org/) to compile C/C++ into JS/WASM and [TeaVM](https://github.com/konsoletyper/teavm) to generate the corresponding Java-to-JavaScript bridge via `@JSBody` annotations. -Currently, jParser supports only `JNI`, `FFM` and `TEAVM` code targets. +## How It Works -## How it Works -jParser consists of two main components: +jParser consists of two main stages: -1. **Code Generation**: It reads the Java source code containing the jParser solution and generates new Java source code for each target platform. The `base` module is used for this purpose. For desktop and mobile platforms, the generated JNI code is located in the `core` module, while the web-specific code is placed in the `teavm` module. +### 1. Code Generation -2. **C/C++ Compilation**: It compiles the C/C++ code for various platforms, including Windows, Linux, macOS, Android, iOS, and the Web. +Reads the hand-written Java source in the `lib-base` module — which contains embedded native code blocks — and generates platform-specific Java source for each target: + +| Output Module | Target | Description | +|---|---|---| +| `lib-core` | JNI | Generated JNI Java for desktop & mobile | +| `lib-teavm` | TeaVM | Generated `@JSBody`-annotated Java for web | +| `lib-desktop-ffm` | FFM | Generated FFM Java for desktop (Java 22+) | -## WebIDL -To further streamline the lengthy process of manually porting each method, jParser includes support for Emscripten WebIDL. By creating a WebIDL file, you can automatically generate binding code for both JNI and TeaVM. While this feature may not cover every scenario, it significantly reduces the effort required to bind large libraries. For a comprehensive example, refer to the `examples:TestLib` module. +### 2. Native Compilation -The generated methods will match those defined in the WebIDL file. If the C++ code is case-sensitive, as seen in ImGui, the corresponding Java methods will also maintain case sensitivity. Additionally, C/C++ attributes are converted into methods prefixed with `set_` or `get_`. +Compiles the C/C++ source into platform-specific native libraries: -## WebIDL Notes -* IDL classes, such as IDLInt or IDLIntArray, provide a way to pass primitive pointers to C++ code. These classes are compatible with Emscripten, desktop, and mobile platforms. Use them when you need to pass a pointer array or a primitive that the C++ code will modify. -* C++ enums are converted into Java Enums, where each enum name contains the integer value from the native code. -* Methods annotated with [Value] return a copy of the object. The object is cached in both C++ and Java. Each time you call the same method, it overwrites the previous data, so avoid retaining references to the returned object. -* JParser does not automatically dispose C++ objects. It's your responsibility to call dispose to free the memory. For classes marked with [NoDelete], there is no need to call dispose. +| Platform | Toolchain | +|---|---| +| Windows | MinGW64 or MSVC | +| Linux | GCC / G++ | +| macOS | Xcode CLI tools | +| Android | Android NDK | +| Web | Emscripten SDK | + +## Supported Targets + +| Target | Bridge | Platforms | Java Version | +|---|---|---|--------------| +| **JNI** | Java Native Interface | Windows, Linux, macOS, Android | 8+ | +| **FFM** | Foreign Function & Memory API | Windows, Linux, macOS | 22+ | +| **TeaVM** | JavaScript / WASM | Web browsers | 11+ | -## IDLBase methods -Every native class extends IDLBase, a parent class that provides common functionality. One commonly used method is dispose, which frees the memory allocated for a native object to prevent memory leaks. -You must call this method when you're done using it. However, not all classes require calling dispose. Only objects you create manually, or those created by a library and explicitly owned by you, need to have their dispose method called. -Creating a native class and disposing is expensive, so avoid calling these operations every frame.

-Here is a list of all IDLBase methods: -* **static [ClassName].native_new()**: Creates a new empty instance without any associated native data. -* **static [ClassName].NULL**: Returns a NULL instance. Every method parameter must not be null. Use this when a native methods needs a null parameter. -* **dispose()**: Deletes the native instance, but only if you own it. -* **isDisposed()**: Checks whether the native instance has been disposed. -* **native_setVoid(...)**: Sets an integer or long memory address. In TeaVM, the long is cast to an integer. -* **native_reset()**: Resets the Java instance to its default state, removing any associated native data. -* **native_takeOwnership()**: Takes ownership of the native data, enabling dispose() to delete the object. -* **native_releaseOwnership()**: Releases ownership of the native data, preventing dispose() from deleting the object. -* **native_hasOwnership()**: Checks whether you own the native instance. -* **native_copy(...)**: Copies the memory address and all other native data from another Java instance to this Java instance. +## Code Block Convention -The **native** method keyword is primarily used to avoid conflicts with C/C++ methods. +In `lib-base` Java source files, native code is embedded via annotated comment blocks. jParser reads these blocks and generates the appropriate code for each target. -## Libraries using jParser:
-- [jWebGPU](https://github.com/xpenatan/jWebGPU)¹ -- [xImGui](https://github.com/xpenatan/xImGui)¹ -- [xJolt](https://github.com/xpenatan/xJolt)¹ -- [xLua](https://github.com/xpenatan/XLua)¹ -- [xBullet](https://github.com/xpenatan/xBullet)¹ -- [gdx-box2d](https://github.com/xpenatan/gdx-box2d)² -- [gdx-physx](https://github.com/xpenatan/gdx-physx)² +```java +public class MyLib extends IDLBase { -¹: The focus is on maintaining this project.
-²: This project is currently inactive and may only be used to test the generator. + // JNI native code block — compiled into C++ for desktop & mobile + /*[-JNI;-NATIVE] + MyType* obj = (MyType*)this_addr; + return obj->getValue(); + */ + private static native int internal_native_getValue(long this_addr); -## Requirements: -#### [Mingw64](https://github.com/niXman/mingw-builds-binaries/releases) or [Visual Studio C++](https://visualstudio.microsoft.com/vs/community/) -#### [Emscripten](https://emscripten.org/) -For Windows builds using WindowsMSVCTarget, ensure `vcvarsall.bat` is accessible via the system PATH. It is typically located in `C:\Program Files\Microsoft Visual Studio\[Year]\[Edition]\VC\Auxiliary\Build\`. + // TeaVM replacement — generates @JSBody-annotated method for web + /*[-TEAVM;-REPLACE] + @org.teavm.jso.JSBody(params = {"this_addr"}, + script = "var jsObj = [MODULE].wrapPointer(this_addr, [MODULE].MyType);" + + "return jsObj.getValue();") + private static native int internal_native_getValue(int this_addr); + */ +} +``` + +### Available Commands + +| Command | Description | +|---|---| +| `-NATIVE` | Inline C/C++ code compiled for the target | +| `-ADD` | Adds code to the generated output | +| `-ADD_RAW` | Adds raw code without processing | +| `-REMOVE` | Removes code from the generated output | +| `-REPLACE` | Replaces the following method with the block content | +| `-REPLACE_BLOCK` | Replaces the following code block | +| `-IDL_SKIP` | Placed on a class comment to skip IDL generation for that class | + +## WebIDL Bindings + +To reduce the effort of manually porting each method, jParser supports **Emscripten WebIDL**. Define a `.idl` file and jParser automatically generates binding code for all targets. + +```idl +interface NormalClass { + void NormalClass(); + long addIntValue(long value1, long value2); + static long subIntValue(long value1, long value2); + attribute long intValue; + attribute float floatValue; +}; +``` + +This generates fully working Java classes with native bindings for JNI, FFM, and TeaVM — no manual glue code required. + +### WebIDL Notes + +- **IDL helper classes** (`IDLInt`, `IDLIntArray`, etc.) let you pass primitive pointers to C++. They work across Emscripten, desktop, and mobile. +- **C++ enums** are converted into Java enums, each carrying the integer value from native code. +- **`[Value]` methods** return a cached copy of the object. The cache is overwritten on each call — do not retain references. +- **`[NoDelete]` classes** should not have `dispose()` called. All other classes require explicit disposal. + +## IDLBase API + +Every native class extends `IDLBase`, which provides common memory-management functionality. + +> **Important:** jParser does not automatically dispose C++ objects. You must call `dispose()` when you're done with an object to free native memory. Only objects you create or explicitly own require disposal. Creating and disposing native objects is expensive — avoid doing it every frame. + +| Method | Description | +|---|---| +| `ClassName.native_new()` | Creates an empty instance without native data | +| `ClassName.NULL` | Returns a NULL instance — use instead of Java `null` for native parameters | +| `dispose()` | Deletes the native instance (only if owned) | +| `isDisposed()` | Checks whether the native instance has been disposed | +| `native_setVoid(...)` | Sets an integer or long memory address | +| `native_reset()` | Resets the instance to default state | +| `native_takeOwnership()` | Takes ownership, enabling `dispose()` to delete the object | +| `native_releaseOwnership()` | Releases ownership, preventing `dispose()` from deleting | +| `native_hasOwnership()` | Checks whether you own the native instance | +| `native_copy(...)` | Copies memory address and native data from another instance | + +> The `native_` prefix is used to avoid naming conflicts with C/C++ methods. + +## Requirements + +| Requirement | Purpose | +|---|---| +| **JDK 11+** | Building jParser tool modules | +| **JDK 22+** (25 recommended) | FFM modules and FFM-based apps | +| [MinGW64](https://github.com/niXman/mingw-builds-binaries/releases) or [Visual Studio C++](https://visualstudio.microsoft.com/vs/community/) | Windows native builds | +| GCC / G++ | Linux native builds | +| Xcode CLI tools | macOS native builds | +| [Emscripten SDK](https://emscripten.org/) | Web builds (JS/WASM) | + +> **Windows (MSVC):** Ensure `vcvarsall.bat` is on your system `PATH`. It is typically located at: +> `C:\Program Files\Microsoft Visual Studio\[Year]\[Edition]\VC\Auxiliary\Build\` + +## Getting Started + +For a complete working example, refer to the [`examples/TestLib`](examples/TestLib) module. + +### Module Layout + +jParser projects follow a strict `-base / -build / -core / -teavm` convention: + +| Module Suffix | Purpose | +|---|---| +| `lib-base` | Hand-written Java source with embedded native code blocks | +| `lib-build` | Build entry point — configures IDL, targets, runs generation + compilation | +| `lib-core` | **Generated** JNI Java output _(do not hand-edit)_ | +| `lib-teavm` | **Generated** TeaVM Java output _(do not hand-edit)_ | +| `lib-desktop-ffm` | **Generated** FFM Java output _(do not hand-edit)_ | +| `lib-desktop-jni` | Bundles JNI-compiled native libraries into a JAR | +| `lib-android` | Android-specific packaging | + +### Build Example: TestLib + +```bash +# 1. Build idl-helper (required once) +./gradlew :idl-helper:idl-helper-build:idl_helper_build_project_jni_windows64 + +# 2. Generate code + compile native library +./gradlew :examples:TestLib:lib:lib-build:TestLib_build_project_jni_windows64 + +# 3. Run the desktop app +./gradlew :examples:TestLib:app:desktop-jni:TestLib_run_app_desktop +``` + +> Replace `windows64` with `linux64`, `mac64`, or `macArm` for other platforms. +> Replace `jni` with `ffm` for Foreign Function & Memory API targets. + +## Libraries Using jParser + +| Library | Description | Status | +|---|---|---| +| [jWebGPU](https://github.com/xpenatan/jWebGPU) | WebGPU bindings for Java | Active | +| [xImGui](https://github.com/xpenatan/xImGui) | Dear ImGui bindings for Java | Active | +| [xJolt](https://github.com/xpenatan/xJolt) | Jolt Physics bindings for Java | Active | +| [xLua](https://github.com/xpenatan/XLua) | Lua bindings for Java | Active | +| [xBullet](https://github.com/xpenatan/xBullet) | Bullet Physics bindings for Java | Active | +| [gdx-box2d](https://github.com/xpenatan/gdx-box2d) | Box2D bindings for libGDX | Inactive | +| [gdx-physx](https://github.com/xpenatan/gdx-physx) | PhysX bindings for libGDX | Inactive | + +## License + +jParser is licensed under the [Apache License 2.0](LICENSE).