Skip to content

FalsePattern/zanama

Repository files navigation

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:

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:

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

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 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.