An open API service indexing awesome lists of open source software.

https://github.com/falsepattern/zanama

A Zig to Java FFI bindings generator, inspired by JExtract.
https://github.com/falsepattern/zanama

bindings-generator gradle-plugin java-ffi java-panama zig-package

Last synced: 4 months ago
JSON representation

A Zig to Java FFI bindings generator, inspired by JExtract.

Awesome Lists containing this project

README

          

# Zanama

A Zig to Java FFI bindings generator, inspired by JExtract.

## Usage

## Step 1: Add the gradle plugin to your repo

```kotlin
plugins {
id("com.falsepattern.zanama") version "ZANAMA_VERSION"
id("com.falsepattern.zigbuild") version "ZIGBUILD_VERSION" //Optional, but it's highly recommended you use this plugin for compiling zig via gradle.
}
```

## Step 2: Create a zig build gradle task

```kotlin
// The directory where zig will output the generated json and binaries
val zigPrefix = layout.buildDirectory.dir("zig-out")
val zigInstall = tasks.register("zigInstall") {
options {
steps.add("install")
}
prefixDirectory = zigPrefix
clearPrefixDirectory = true
sourceFiles.from(
// Any files/directories that are referenced by your zig buildscript, as well as:
layout.buildDirectory.dir("zanama"),
)
//This task provides the Zig side of the bindings generator
dependsOn("extractZanama")
}
```

## Step 3: Set up the zig build for generating the binaries and the json output

The following is a small example of how the zig build side of zanama can be used. This will require changes based on how your project is actually structured.

### `build.zig.zon`:

```zig
.{
.dependencies = .{
.zanama = .{ .path = "build/zanama" }, //This path is generated by the extractZanama gradle task.
},
}
```

### `build.zig`:

```zig
const std = @import("std");
const zanama = @import("zanama");

// This creates a single linux x86_64 baseline binary
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});

// Zanama needs to be aware of itself as the dependency, as well as the main build instance
const zanama_dep = b.dependency("zanama", .{});
const zb = zanama.Build.init(b, optimize, zanama_dep);

// Your code
const root_module = b.createModule(.{
.root_source_file = b.path("src/main/zig/root.zig"),
.imports = &.{
.{ .name = "zanama", .module = zanama_dep.module("api") },
},
});

// Generating zanama bindings, as well as the libraries.
// There are other createZanamaLibs* functions, check the source code for more info
const libs = zb.createZanamaLibsQuery("root", root_module, &.{zanama.target.common.x86_64.linux.baseline});

// Putting the libraries and generated json into the prefix directory (specified in the gradle task)
const install_step = b.getInstallStep();
for (libs.artifacts) |artifact| {
install_step.dependOn(&b.addInstallArtifact(artifact, .{
//Configured like this to make the gradle side simpler
.dest_dir = .{ .override = .lib },
.h_dir = .disabled,
.implib_dir = .disabled,
.pdb_dir = .disabled,
}).step);
}
const install_json = b.addInstallFile(libs.json, "root.json");
install_json.step.dependOn(libs.json_step);
install_step.dependOn(&install_json.step);
}

```

### `src/main/zig/root.zig`:

```zig
const zanama = @import("zanama");

pub const Bar = extern struct {
x: i32,
y: i32,
z: i32,

// Any function you want to call from Java must have the C calling convention.
pub fn foo(_: NonPacked) callconv(.c) void {

}
};

comptime {
zanama.genBindings(&.{
.{ .name = "myproject.Bar", .Struct = Bar },
}) catch unreachable;
}

```

## Step 4: Processing the zig build outputs in gradle
```kotlin
val translateJavaSources = layout.buildDirectory.dir("generated/zanama_root")

val zigTranslate = tasks.register("zigTranslate") {
// zigPrefix variable specified in step 2
from = zigPrefix.map { it.file("root.json") }
into = translateJavaSources
//The package you want to put the generated code into. May create sub-packages for nested structs, but never above this package.
rootPkg = "com.example.myproject.natives"
//The prefix of the .name part in the zanam genBindings call for all the bindings you added
bindRoot = "myproject"
//The name of the "main" class of this translation unit. It contains shared type info used by all bindings in the json
className = "root_z"
dependsOn(zigInstall) // from step 2
}

sourceSets["main"].java.srcDir(translateJavaSources)

tasks.compileJava {
dependsOn(zigTranslate)
}

//You can either package the output DLLs into your program, include it in the generated jar file, or whatever else
tasks.processResources {
dependsOn(zigInstall)
into("com/example/myproject/natives") {
from(zigPrefix.map { it.dir("lib") } )
include("*.dll", "*.so", "*.dylib") // windows, linux, macos
}
}

//Add the generated libraries to your jar

//Pull in the zanama runtime.
dependencies {
implementation("com.falsepattern:zanama-rt:ZANAMA_VERSION")
}
```

## Step 5: Create the initializer class

The generated main class of the translation requires you to load the native library manually.

`root_z_init.java`:
```java
class root_z_init {
//You can use the unpacker for jar-bundled resources (CAREFUL: gradle's application `run` task DOES NOT run the jar, so createWithUnpacker won't work there! Make a custom gradle task that runs your jar if you want to use it)
//Alternatively, you can ship the natives already unpacked, and use NativeContext.create(Path)
//You can share a single NativeContext between all *_init classes as long as you package all the natives into the same directory
private static final NativeContext CTX = NativeContext.createWithUnpacker(Path.of("natives"), "com/example/myproject/natives/");

public static NativeContext.Lib createLib() {
// You can do this checked and handle the errors yourself, etc.
// For now, Zanama is not flexible enough for a better architecture, so this is fine.

// Determining the library name is up to the user.
// You can use the Platform class as extra help for determining the OS/CPU.
var name = "root-linux-x86_64-baseline";
return CTX.loadUnchecked(root_z_init.class, name);
}
}
```

At this point, you should be able to use the generated zanama bindings to work with structs and call methods.

## Notes/todos

- Zanama is not meant for generating bindings to arbitrary libraries, and instead is designed as the "inverse" of JNI headers. This means that it should not be used for
mass-binding random libraries, but instead done with "pinhole" bindings for code that you directly control. If you want to do mass bindings, use JExtract with C headers,
it's way more robust for that use case.
- Zanama currently uses global state. Once a library is loaded, it can never be unloaded until the process exits, or you unload the zanama classes and discard the arena (accessible with the alternative NativeContext methods)
- Translation for struct default values are not yet implemented
- Non-type/function constants (`pub const x: u32 = 123;`) crash the translator. This is a known issue.