Zygisk API bindings for Zig. Write Magisk Zygisk modules in pure Zig — no C++ required.
Rust has zygisk-api-rs. Zig had nothing. Now it does.
- Pure Zig — no C++ compiler, no
zygisk.hpp, no-static-libstdc++ - Comptime registration —
zygisk.register(MyModule)generates the export symbol - Full API coverage — all 10 Zygisk API methods + JNI helpers
- Both ABIs —
arm64-v8a+armeabi-v7afrom a singlezig build
- Zig 0.16+
- Android NDK r25+
- Magisk with Zygisk enabled
git clone https://github.com/lunebit/zigysk
cd zigysk
# Build (auto-detects NDK from $ANDROID_NDK_HOME)
zig build -Doptimize=ReleaseSafe
# Package Magisk module zip
zig build zip -Doptimize=ReleaseSafe
# Deploy to device
adb push zig-out/zigysk_example.zip /data/local/tmp/
adb shell su -c "magisk --install-module /data/local/tmp/zigysk_example.zip"
adb rebootAlternatively, you can use the build.sh script to automatically deploy to your device. Once the deployment is complete, the device will restart.
# Check logs
adb logcat -s ZigyskTest
Before developing, you should check out the official documentation for Magisk Modules.
const std = @import("std");
const zygisk = @import("zygisk");
const MyModule = struct {
pub fn onLoad(api: *zygisk.Api, env: *zygisk.JNIEnv) void {
// Called when module loads into a process.
}
pub fn preAppSpecialize(api: *zygisk.Api, args: *zygisk.AppSpecializeArgs) void {
// Read package name
const env = zygisk.getEnv();
const jstr: zygisk.JString = args.nice_name.*;
if (zygisk.getUtf8String(env, jstr, std.heap.c_allocator)) |result| {
if (result) |name| {
defer std.heap.c_allocator.free(name);
// name is [:0]u8 — use as C string
if (std.mem.eql(u8, name, "com.example.target")) {
// Do something for this app
}
}
} else |_| {}
}
pub fn postAppSpecialize(api: *zygisk.Api, args: *zygisk.AppSpecializeArgs) void {
// Called after app sandbox is active.
}
};
comptime {
zygisk.register(MyModule);
}All callbacks are optional — declare only what you need.
| Callback | Privilege | When |
|---|---|---|
onLoad(api, env) |
Zygote → App | Module loaded into process |
preAppSpecialize(api, args) |
Zygote (root) | Before app fork specialization |
postAppSpecialize(api, args) |
App sandbox | After specialization |
preServerSpecialize(api, args) |
Zygote (root) | Before system_server specialization |
postServerSpecialize(api, args) |
system_server | After system_server specialization |
pub fn onLoad(api: *zygisk.Api, env: *zygisk.JNIEnv) void
pub fn preAppSpecialize(api: *zygisk.Api, args: *zygisk.AppSpecializeArgs) void
pub fn postAppSpecialize(api: *zygisk.Api, args: *zygisk.AppSpecializeArgs) void
pub fn preServerSpecialize(api: *zygisk.Api, args: *zygisk.ServerSpecializeArgs) void
pub fn postServerSpecialize(api: *zygisk.Api, args: *zygisk.ServerSpecializeArgs) voidAll methods check for null function pointers (older Zygisk versions).
// Root companion IPC
api.connectCompanion() // → socket fd (or -1)
// Module directory
api.getModuleDir() // → fd (or -1)
// Process state
api.getFlags() // → u32 (bitmask)
api.isOnDenyList() // → bool
api.isGrantedRoot() // → bool
// Specialization control
api.setOption(.force_denylist_unmount)
api.exemptFd(fd) // → bool
// Hooks
api.hookJniNativeMethods(env, "android/util/Log", &methods, 1)
api.pltHookRegister(dev, inode, "open", @ptrCast(&hookedOpen), &origOpen)
api.pltHookCommit() // → boolfn companionHandler(fd: c_int) void {
// Runs in root daemon process.
// fd is a Unix socket connected to the module in the app process.
// Read/write via standard syscall read()/write().
}
comptime {
zygisk.register(MyModule);
zygisk.registerCompanion(companionHandler);
}All required fields are pointers (C++ references). Access via .field.*.
args.uid.* // i32 — process UID
args.gid.* // i32 — process GID
args.nice_name.* // JString — package name (use getUtf8String to read)
args.app_data_dir.* // JString — /data/data/<package>
args.runtime_flags.* // i32
args.is_top_app.* // u8 (optional — check != null first)JNI types are opaque — we access the C++ _JNIEnv function table by stable index.
// String operations
zygisk.getUtf8String(env, jstr, allocator) // → ?[:0]u8 (caller frees)
zygisk.newStringUTF(env, "hello") // → ?JString
// Class and method lookup
zygisk.findClass(env, "java/lang/String") // → ?JClass
zygisk.getMethodID(env, clazz, "length", "()I") // → ?JMethodID
zygisk.getStaticMethodID(env, clazz, "valueOf", "(I)Ljava/lang/String;") // → ?JMethodID
// References
zygisk.newGlobalRef(env, obj) // → JObject
zygisk.deleteGlobalRef(env, obj) // void
// Exceptions
zygisk.exceptionCheck(env) // → bool
zygisk.exceptionClear(env) // void
zygisk.throwNew(env, clazz, "error") // → c_int
// JavaVM
zygisk.getJavaVM(env) // → ?*JavaVM
zygisk.getJavaVM_global() // → ?*JavaVM (saved during onLoad)Zero-allocation logging via stack buffer (256 bytes). No malloc, no OOM risk. Format strings are comptime-checked by the Zig compiler.
// Set tag (optional, defaults to "ZygiskModule")
zygisk.Log.setTag("MyModule");
// Log with Zig-style format specifiers (not C %d/%s)
zygisk.Log.info("package: {s} uid={d}", .{ name, uid });
zygisk.Log.warn("unexpected fd: {d}", .{fd});
zygisk.Log.err("JNI failed: {}", .{err});
zygisk.Log.debug("hook at 0x{x}", .{addr}); // requires debug build / your own gateOutput in logcat:
I MyModule: package: com.example.app uid=10149
W MyModule: unexpected fd: -1
E MyModule: JNI failed: error.JniGetStringFailed
# Override NDK location
zig build -Dndk=/path/to/sysroot
# Override API level
zig build -Dapi=33
# Release mode
zig build -Doptimize=ReleaseSafe
# Package Magisk module zip
zig build zip
# Auto-detect NDK from $ANDROID_NDK_HOME
export ANDROID_NDK_HOME=/path/to/ndk
zig buildzig-out/
├── module.prop
└── zygisk/
├── arm64-v8a.so ← your module (Zygisk expects this name)
└── armeabi-v7a.so
my_module/
├── module.prop
└── zygisk/
├── arm64-v8a.so
└── armeabi-v7a.so
Copy zygisk.zig into your project and import it:
// build.zig
const zygisk_mod = b.addModule("zygisk", .{
.root_source_file = b.path("path/to/zygisk.zig"),
});
// In your module:
const zygisk = @import("zygisk");Or use the example build.zig as a template for multi-ABI builds.
This library targets Zygisk API version 4 (Magisk 25.2+).
This project is actively maintained, and contributions are welcome.
You can help by:
- reporting bugs;
- improving documentation;
- adding examples;
- extending the bindings;
- improving compatibility with new Zygisk releases.
Feel free to open an issue or submit a pull request.
MIT
This project provides language bindings only. Users are responsible for how they use these APIs and must comply with applicable laws and software licenses.