Skip to content

Latest commit

 

History

History
297 lines (218 loc) · 7.9 KB

File metadata and controls

297 lines (218 loc) · 7.9 KB

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.