A Zig to Java FFI bindings generator, inspired by JExtract.
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.
}
// 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")
}
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.
.{
.dependencies = .{
.zanama = .{ .path = "build/zanama" }, //This path is generated by the extractZanama gradle task.
},
}
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);
}
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;
}
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")
}
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 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.
- 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.