A Zig to Java FFI bindings generator, inspired by JExtract.
- Kotlin 45.2%
- Zig 28.2%
- Java 26.6%
Zanama
A Zig to Java FFI bindings generator, inspired by JExtract.
Usage
Step 1: Add the gradle plugin to your repo
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
// The directory where zig will output the generated json and binaries
val zigPrefix = layout.buildDirectory.dir("zig-out")
val zigInstall = tasks.register<ZigBuildTask>("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:
.{.dependencies=.{.zanama=.{.path="build/zanama"},//This path is generated by the extractZanama gradle task.},}build.zig:
conststd=@import("std");constzanama=@import("zanama");// This creates a single linux x86_64 baseline binarypubfnbuild(b:*std.Build)void{constoptimize=b.standardOptimizeOption(.{});// Zanama needs to be aware of itself as the dependency, as well as the main build instanceconstzanama_dep=b.dependency("zanama",.{});constzb=zanama.Build.init(b,optimize,zanama_dep);// Your codeconstroot_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 infoconstlibs=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)constinstall_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);}constinstall_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:
constzanama=@import("zanama");pubconstBar=externstruct{x:i32,y:i32,z:i32,// Any function you want to call from Java must have the C calling convention.pubfnfoo(_:NonPacked)callconv(.c)void{}};comptime{zanama.genBindings(&.{.{.name="myproject.Bar",.Struct=Bar},})catchunreachable;}Step 4: Processing the zig build outputs in gradle
val translateJavaSources = layout.buildDirectory.dir("generated/zanama_root")
val zigTranslate = tasks.register<ZanamaTranslate>("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:
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 directoryprivatestaticfinalNativeContextCTX=NativeContext.createWithUnpacker(Path.of("natives"),"com/example/myproject/natives/");publicstaticNativeContext.LibcreateLib(){// 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.varname="root-linux-x86_64-baseline";returnCTX.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.