Skip to content

Grafting: Foreign Code Integration

Injecting the world’s code into your AI-first applications


Grafting is Janus’ approach to foreign code integration. Unlike traditional “bindings” or “wrappers”, grafting allows you to seamlessly inject foreign libraries and runtimes directly into your Janus code while maintaining:

  • Zero-cost abstraction - No runtime overhead
  • Cryptographic security - Capability-based access control
  • AI tooling integration - Self-documenting code with UTCP manuals
  • Namespace hygiene - Contained under std.graft.*

Result: You can use any Zig library or Python module as if it were native Janus code.



Janus is built on Zig. You are not just grafting; you are extending the compiler substrate.

Use Zig through explicit, auditable boundaries:

  • graft zig "path.zig" as name for a Zig file that exports C-ABI functions.
  • janus graft regenerate to materialize checked-in Janus wrappers under graft/zig/.
  • Inline graft zig do ... end only for deliberate embedded-Zig escape hatches.

Ordinary long-lived Janus modules should prefer use std.* or generated wrappers. They should not grow broad use zig imports as a substitute for a native Janus surface.

Run the generator whenever static graft zig declarations change:

graft zig "std/sys/linux/time.zig" as time

Then regenerate:

Terminal window
janus graft regenerate
janus graft regenerate --check

The committed output lives in graft/zig/. Each wrapper has a generated-from-graft: header, calls the explicit graft alias, and tags exported Zig functions with {.profile: sovereign.} until finer capability inference lands.

Import any .zig file from your project structure.

// Graft a local Zig file
graft zig "./vendor/math_utils.zig" as math
func compute() f64 do
return math.fast_inverse_sqrt(16.0)
end

When the Zig file lives in a different repository, declare the external root via the JANUS_ZIG_ROOTS environment variable — a colon-separated list of absolute directories, tried in order after the local strategies (absolute, source-relative, std/ under the Janus root, bare std/<name> forms) and before the cwd fallback:

Terminal window
export JANUS_ZIG_ROOTS=/path/to/kernel/src:/path/to/other/repo
// Resolves to /path/to/kernel/src/skills/propose.zig
use zig "skills/propose.zig"

The first root containing the declared relative path wins. Resolution is deterministic — no symlinks and no dependence on the build’s working directory — so worktree-based and CI builds stay reproducible.

A grafted Zig module may re-export a type from another file and use the alias as a graft type — no direct graft of the underlying file needed:

defs.zig
pub const Inner = struct { state: i64, ... };
pub const ByteSlice = extern struct { ptr: [*]const u8, len: usize };
// mod.zig (the grafted file)
pub const Foo = @import("defs.zig").Inner; // import re-export
pub const Bar = Foo; // same-file alias
pub const Bytes = @import("defs.zig").ByteSlice;
pub export fn give_bytes() Bytes { ... }

The alias behaves exactly like the underlying struct: extern fn param/return types (including slice-shaped extern struct {ptr, len} returns, which materialize as native Janus slices) and method resolution (mod.Foo.method). @import following is limited to paths relative to the grafted file — Zig-stdlib (std, std/...) and absolute imports stay unresolvable and are skipped silently. Non-struct re-exports (constants, functions) are not followed.

Zig-CC Methods and Comptime Generics: Trampoline Sidecars (Gap 109b)

Section titled “Zig-CC Methods and Comptime Generics: Trampoline Sidecars (Gap 109b)”

Zig struct methods declared plain pub fn (Zig calling convention, not pub export fn) and comptime-generic free functions (pub fn max(comptime T: type, ...)) have no C-ABI symbol of their own. When a Janus compilation unit actually calls one, the compiler now synthesizes a C-ABI trampoline sidecar — a generated .zig with pub export fn janus_zigcc_tramp_<...>(...) wrappers — compiles it through the same zig build-obj pipeline stage as the grafted module, and rewrites the call to the wrapper. No hand-written sidecar is needed for the covered shapes:

// widget.zig (the grafted file)
pub const Widget = struct {
state: i64,
pub fn ping(self: *Widget) i64 { return self.state; } // Zig-CC
};
pub fn gmax(comptime T: type, a: T, b: T) T { // generic
return if (a > b) a else b;
}
graft zig "widget.zig" as probe
let w = probe.widget_make(10)
let v = probe.Widget.ping(w) // trampoline: scalar/pointer signature
let m = probe.gmax(7, 3) // trampoline: gmax(i64, 7, 3)

Scope, honestly:

  • Covered: Zig-CC methods whose params and return are C-ABI scalars, sentinel strings, pointers, or optionals (the receiver is passed explicitly, e.g. probe.Widget.ping(w)); comptime-generic free functions of the exact shape f(comptime T: type, ...) -> T, instantiated from the first argument’s type (one wrapper per concrete type).
  • Still rejected (E2410): slice params/returns at the trampoline boundary, error-union returns, comptime-generic methods, generics with more than one comptime parameter or mixed parameter types. For those, keep writing a pub export fn sidecar wrapper by hand.

// Graft a checked-in bridge file.
graft zig "std/sys/linux/time.zig" as time
func main() do
let now = time.stp_now_ns()
println(now)
end

The foreign Zig module is available under the explicit alias. For public bridge surfaces, commit the generated wrapper and keep application code on the Janus side of the boundary.

func secureProcess(path: string, ctx: Context) !Result do
// Janus enforces capability checks automatically
let data = std.fs.readFile(path, ctx.fs_read_capability);
let result = my_zig_library.process(data);
return result;
end

Foreign code cannot access your filesystem without explicit capability grants.


// Create a Python runtime instance
foreign "python" as py do
import numpy as np
import matplotlib.pyplot as plt
def analyze_data(data):
return {
'mean': np.mean(data),
'std': np.std(data),
'plot': plt.histogram(data)
}
end
// Use it seamlessly in Janus
func processSensorData(raw_data: []f64, ctx: Context) !Analysis do
using py := try std.graft.python.open(ctx, cap: ctx.foreign.python.ipc) do
// Convert Janus arrays to Python
let py_array = py.call("numpy.array", [raw_data]);
// Call our custom Python function
let analysis = try py.call("analyze_data", [py_array]);
// Extract results back to Janus types
return Analysis{
mean: try analysis.get("mean").to_number(),
std: try analysis.get("std").to_number(),
histogram_data: try analysis.get("plot").to_array()
};
end
end

You just seamlessly called Python’s NumPy and Matplotlib from Janus code!


Status: merged to unstable at a2780174 (PR #34, Gaps 195, 201 and 202) on 2026-10-11; not yet in a tagged release. Every example below was compiled with janus build and run on Linux x86_64. The compiler prints the full source path in front of each location; the transcripts below shorten it to the file name.

graft c "header.h" as alias translates a C header and exposes its functions under the alias (graft alias = c "header.h" is the same declaration). The compiler calls them with exactly the parameter types and order the header declares, and it refuses a call whose ABI it cannot produce faithfully instead of compiling a wrong one.

C prototypes may omit parameter names. Put this in prototype.h:

int abs(int);
int strncmp(const char *, const char *right, unsigned long);
graft c "prototype.h" as libc
func main() do
print_int(libc.abs(-7))
print_int(libc.strncmp("sameA", "sameB", 4))
end

It prints 7, then 0. Before PR #34 an unnamed parameter lost its call argument and libc.abs(-7) printed an unrelated number. restrict pointer parameters, named or unnamed, keep their pointer type as well.

Variadic functions are refused at the call (E2413)

Section titled “Variadic functions are refused at the call (E2413)”

Janus does not implement the C variadic calling convention. Put this in stdio_min.h:

int printf(const char *, ...);
int puts(const char *);

Save the program as variadic_call.jan next to the header:

graft c "stdio_min.h" as libc
func main() do
libc.puts("fixed arity works")
libc.printf("%d\n", 7)
end

The build fails before any native code is emitted, and no binary is written:

variadic_call.jan:5:5: error: E2413: variadic C graft calls are not supported; expose a fixed C-ABI wrapper for 'libc.printf'
Lowering error: UnsupportedForeignABI

Delete the printf line and the same header builds: puts prints fixed arity works. The variadic declaration is harmless; only calls to it are refused. That covers direct calls, calls in defer and errdefer, top-level calls, calls in imported modules, and even a call inside an imported function that nothing uses. Variadic extern fn declarations that use zig or graft zig read from a Zig file follow the same rule. Before PR #34 such a call compiled and was lowered as if the function had a fixed parameter list.

Give the variadic function a fixed signature on the other side of the border. A Zig adapter keeps the variadic call where the C convention is implemented. Put this in fmt_adapter.zig:

extern fn printf(format: [*:0]const u8, ...) c_int;
pub export fn print_count(label: [*:0]const u8, count: c_long) callconv(.c) c_int {
return printf("%s: %ld\n", label, count);
}
graft zig "fmt_adapter.zig" as fmt
func main() do
println("before")
fmt.print_count("files", 3)
println("after")
end

It prints before, files: 3, after. Janus calls print_count, whose parameters are fixed; it never calls printf itself, so E2413 does not apply.

Variadic function pointers are refused too

Section titled “Variadic function pointers are refused too”

A function pointer can carry an ellipsis inside a parameter or return type. Put this in logger.h:

void set_logger(int (*log)(const char *, ...));
long (*get_sum(void))(long, ...);
int abs(int);

Save the program as fnptr_param.jan next to the header:

graft c "logger.h" as clog
func log(x: i64) -> i64 do return x end
func main() do
clog.set_logger(log)
end
fnptr_param.jan:6:5: error: E2413: variadic C function pointers cannot cross the graft boundary; expose a fixed C-ABI adapter for 'clog.set_logger'
Lowering error: UnsupportedForeignABI

let sum = clog.get_sum() fails the same way and names 'clog.get_sum'. Janus func(...) values and indirect calls carry only fixed-arity calling conventions, so a pointer to a variadic function would be called with the wrong ABI. Write a fixed adapter that forwards to the variadic function, as above. Function pointers with fixed parameter lists remain usable, and the unused declarations do not affect the rest of the header: clog.abs(-7) builds and prints 7.

  • The contract applies to graft c, use zig and graft zig.
  • Aggregate (struct-by-value) ABI support is unchanged by this work.
  • A grafted function cannot be passed by name as a func value: apply(libc.abs, -7) fails with error: no exported symbol in imported module 'abs'.
  • The C-graft subprocess launcher supports Linux hosts; other hosts fail with UnsupportedGraftHost.
  • Native regressions: ./scripts/zb test-c-header-discovery in the compiler repository.

Status: the forms in this section (graft libc = c "libc.so.6", foreign "c" as name { ... }) are design sketches, not verified compiler paths. The verified C form is graft c "header.h" as alias, optionally with link "library"; see C Header Grafts: The Signature Contract.

Janus treats C as the “lingua franca” of the system. You can graft shared objects (.so, .dll, .dylib) directly.

// Graft a standard C library
graft libc = c "libc.so.6";
func getCurrentTime() i64 do
// Call C functions directly with zero overhead
return libc.time(null);
end

For complex C libraries, you can provide inline C definitions to help the Janus compiler understand types.

foreign "c" as raylib {
#include "raylib.h"
// Janus automatically parses the header AST
}
func drawGame() do
raylib.InitWindow(800, 600, "Janus Window");
while !raylib.WindowShouldClose() do
raylib.BeginDrawing();
raylib.ClearBackground(raylib.RAYWHITE);
raylib.DrawText("Hello C-Realms!", 190, 200, 20, raylib.LIGHTGRAY);
raylib.EndDrawing();
end
raylib.CloseWindow();
end

Janus can drink directly from the Cargo stream. This requires cargo to be available in the build environment.

// Graft a Rust crate directly from a git repo
graft rocket = cargo "https://github.com/rwf2/Rocket";
// Or specify a crate version (uses crates.io)
graft serde = cargo "serde:1.0";

Janus respects Rust’s safety guarantees.

// Using the grafted Rust library
func handleRequest(req: Request) !Response do
// Rust's Results are mapped to Janus error unions automatically
let processed = try rocket.process(req);
return processed;
end

Note: The first time you build, Janus will invoke Cargo to compile the grafted crate as a static library, then link it.


For high-performance numerical computing, nothing beats Julia.

foreign "julia" as flux do
using Flux
using LinearAlgebra
function train_model(x_data, y_data)
model = Chain(Dense(10, 5, relu), Dense(5, 2))
loss(x, y) = Flux.mse(model(x), y)
Flux.train!(loss, Flux.params(model), [(x_data, y_data)], ADAM())
return model
end
end

Janus Arrays ([]f64) map directly to Julia Arrays with zero checking.

func runTraining() !Model do
// Capability: Requires 'julia.embed' capability
using jl := try std.graft.julia.open(ctx, cap: ctx.foreign.julia.embed) do
let x = std.tensor.random([10, 100]);
let y = std.tensor.random([2, 100]);
// Pass Janus tensors to Julia
// Memory is shared, not copied!
let model = jl.call("train_model", [x, y]);
return model;
end
end

// Graft BLAS library for linear algebra
graft zig "vendor/blas_bridge.zig" as blas
func matrixMultiply(a: Matrix, b: Matrix) Matrix do
// Direct call to optimized BLAS
return blas.gemm(a, b, 1.0, 0.0)
end
foreign "python" as scraper do
import requests
from bs4 import BeautifulSoup
def extract_titles(url):
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
return [h1.text for h1 in soup.find_all('h1')]
end
func scrapeHeadlines(url: string, ctx: Context) ![]string do
using scraper := try std.graft.python.open(ctx, cap: ctx.foreign.python.ipc) do
let titles = try scraper.call("extract_titles", [url]);
return titles.to_array(); // Automatic conversion
end
end
// Graft a crypto library
graft zig "vendor/crypto_bridge.zig" as crypto
// Janus handles capability verification automatically
func encryptData(data: []u8, key: Key) ![]u8 do
return crypto.aead_encrypt(data, key, "JanusData")
end

  • Zero-copy data transfer where possible
  • Native compilation of grafted Zig code
  • Profile optimization (:core vs :sovereign performance modes)
  • Capability tokens required for sensitive operations
  • Sandboxing prevents foreign code escape
  • Audit trails track all foreign function calls
  • Memory Safety: Rust grafts retain borrow checker guarantees at the boundary
  • Julia & Python: Direct memory mapping for tensors (Zero-Copy)
  • Unified Logic: One language (Janus) to orchestrate C, Rust, Python, and Julia
// UTCP manuals make foreign code discoverable
func discoverCapabilities() ![]UTCPManual do
// AI tools can explore all grafted capabilities
return std.graft.enumerateAll();
end

// For algorithm-heavy code
graft zig "path/to/zig_algorithm_bridge.zig" as algorithm
func processLargeDataset(data: Dataset) Result do
// Use optimized parallel algorithms
return algorithm.parallel_process(data)
end
foreign "python" as ecosystem do
import scikit-learn
import pandas as pd
# ... ML pipeline code ...
end
func trainModel(training_data: Dataset) Model do
using ecosystem := std.graft.python.open(...) do
let df = ecosystem.call("pandas.DataFrame", [training_data]);
return ecosystem.call("train_ml_model", [df]);
end
end
// Connect to existing C libraries
graft legacy = c "legacy_system.so";
func migrateLegacyData() MigrationResult do
let raw_data = legacy.fetch_records();
return transformToModernFormat(raw_data);
end

// Automatic cleanup
using py := std.graft.python.open(ctx, cap: ctx.foreign.python.ipc) do
let result = py.call("process_data", [data]);
// py.close() called automatically here
end
let result = py.call("risky_operation", [args]) or do |err|
// Handle Python exceptions in Janus
match err.kind {
.ForeignError => log.warn("Foreign code error: {err.message}");
.CapabilityMissing => return Error.InsufficientPrivileges;
.Timeout => return Error.OperationTimeout;
}
end
// Automatic marshalling
let python_dict = py.eval("{'key': 'value'}");
let janus_table = python_dict.to_table(); // Converts Python dict to Janus table
// Zero-copy for arrays (where supported)
let numpy_array = py.call("numpy.linspace", [0, 100, 50]);
let janus_array = numpy_array.to_ndarray("f64"); // Direct memory mapping

// :core profile - IPC only
using py := std.graft.python.open(ctx, cap: ctx.foreign.python.ipc);
// :sovereign profile - Embed Python VM
using py := std.graft.python.open(ctx, cap: ctx.foreign.python.embed);
janus.policy.kdl
foreign {
python {
deterministic = false // Allow non-deterministic ops in trusted contexts
timeout_ms = 5000
allowed_modules = ["numpy", "scipy", "matplotlib"]
}
}

  1. Capability Missing

    // Add to function parameters
    func myFunc(ctx: Context) do // Context provides capabilities
    let py = std.graft.python.open(ctx, cap: ctx.foreign.python.ipc);
    end
  2. Timeout Errors

    let result = py.call("long_running", args, timeout_ms: 30000);
  3. Type Conversion Issues

    // Explicit conversions
    let py_obj = py.eval("some_python_expression");
    let janus_value = py_obj.to_number() or do return Error.ConversionFailed; end

Grafting is just getting started:

  • More Languages: Rust, Lua, JavaScript, R integration
  • Performance: Direct memory mapping for tensor operations
  • AI Integration: Automatic code generation from UTCP manuals
  • Cloud: Distributed grafting across Janus clusters

With Janus grafting, your code can leverage the entire world’s software ecosystem while maintaining security, performance, and AI-first design principles.