Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

zigysk

Zygisk API bindings for Zig. Write Magisk Zygisk modules in pure Zig — no C++ required.

Why?

Rust has zygisk-api-rs. Zig had nothing. Now it does.

  • Pure Zig — no C++ compiler, no zygisk.hpp, no -static-libstdc++
  • Comptime registrationzygisk.register(MyModule) generates the export symbol
  • Full API coverage — all 10 Zygisk API methods + JNI helpers
  • Both ABIsarm64-v8a + armeabi-v7a from a single zig build

Requirements

Quick Start

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 reboot

Alternatively, 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

Development

Before developing, you should check out the official documentation for Magisk Modules.

Writing a Module

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);
}

Module Callbacks

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

Callback signatures

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) void

Zygisk API

Api methods

All 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()           // → bool

Companion process

fn 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);
}

AppSpecializeArgs

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 Helpers

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)

Logging

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 gate

Output in logcat:

I MyModule: package: com.example.app uid=10149
W MyModule: unexpected fd: -1
E MyModule: JNI failed: error.JniGetStringFailed

Build Configuration

# 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 build

Output structure

zig-out/
├── module.prop
└── zygisk/
    ├── arm64-v8a.so        ← your module (Zygisk expects this name)
    └── armeabi-v7a.so

Magisk module structure

my_module/
├── module.prop
└── zygisk/
    ├── arm64-v8a.so
    └── armeabi-v7a.so

Using zigysk in your own project

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.

Zygisk API Version

This library targets Zygisk API version 4 (Magisk 25.2+).

Contributing

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.

License

MIT

Disclaimer

This project provides language bindings only. Users are responsible for how they use these APIs and must comply with applicable laws and software licenses.

About

Zygisk API bindings for Zig. Write Magisk Zygisk modules in pure Zig — no C++ required.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages