Skip to main content

Nano-FFI

Nano-FFI

CI PyPI Downloads Python 3.10+ Zig 0.15.2 License: MIT

Near-zero-overhead Python-to-Zig FFI. Type-safe call wrappers generated at comptime — no runtime type inspection, no branches in the hot path.


Nano-FFI lets Python call Zig functions at close to native speed. Where ctypes and cffi inspect argument types on every call, Nano-FFI uses Zig's comptime engine to generate a type-safe trampoline for each function at compile time. A scalar call costs about 90 ns — roughly 5× less overhead than ctypes and 2.5× less than cffi — with support for strings, bytes, zero-copy buffers, multi-value returns, and Zig-error-to-Python-exception mapping.

Zig performance. Python brain. Comptime safety.

Why Nano-FFI?

Nano-FFI ctypes cffi PyO3 / Cython
Scalar call overhead ~90 ns ~440 ns ~225 ns compile-time typed
Type check location compile time every call every call compile time
Native language Zig any C ABI any C ABI Rust / C
Build step required pre-built wheels none C compiler Rust / C toolchain
Zero-copy buffers yes manual manual yes
Zig errors → Python exceptions yes no no n/a

Overhead figures call the same native add on all three bridges, measured by this repo's benchmark on one machine; treat them as relative, not absolute.

If you write your native code in Zig and want the thinnest, fastest possible call path into it from Python, Nano-FFI is built for exactly that: the per-function trampoline is generated at comptime, so the hot path has no runtime type inspection and no branches to resolve.

Contents

Install

From PyPI (pre-built wheels, no Zig required):

pip install nano-ffi

Or build from source for the latest.

Quickstart

import nano_ffi

# Scalars
nano_ffi.call("add", 3, 4)          # 7
nano_ffi.call("mul", 2.5, 4.0)      # 10.0

# Strings (UTF-8 preserved)
nano_ffi.call("strlen", "hello")    # 5
nano_ffi.call("echo", "Ñuñoa")      # "Ñuñoa"

# Multiple return values -> tuple
q, r = nano_ffi.call("divmod", 17, 5)   # (3, 2)

# Zero-copy: Zig writes straight into a Python buffer
buf = bytearray(4)
nano_ffi.call("fill", buf, 7)       # buf -> bytearray(b'\x07\x07\x07\x07')

# Zig errors become Python exceptions
try:
    nano_ffi.call("div", 10, 0)
except RuntimeError as e:
    print(e)                        # "DivisionByZero"

# The module is self-describing
nano_ffi.list_functions()           # ['add', 'mul', 'echo', ...]
nano_ffi.signature("add")           # {'args': [('a','i64'),('b','i64')], 'ret': 'i64'}

Supported types

Name Zig type Python type Notes
i64 i32 i64 i32 int range-checked narrowing
u64 u32 u8 u64 u32 u8 int unsigned; out-of-range -> ValueError
f64 f32 f64 f32 float
bool bool bool
str []const u8 str borrowed UTF-8 in, copied out
bytes []const u8 bytes borrowed in, copied out
buffer []u8 writable buffer zero-copy, in-place (argument only)

Up to 8 arguments and 8 return values per call. Full reference: docs/API.md.

How it works

makeTrampoline runs at compile time. It unpacks each argument with a comptime inline for over the signature, so the compiler emits explicit typed assignments — not a runtime loop with per-element type tests. The dispatch path Python calls into has no branch left to resolve.

flowchart LR
  A[Python args] --> B[registry lookup]
  B --> C["comptime trampoline<br/>(branch-free unpack)"]
  C --> D[Zig function]
  D --> E[pack result / error]
  E --> F[PyObject]

Only python_ext.zig touches <Python.h>; everything else is pure Zig over C-ABI-compatible types, so the core is unit-tested with no interpreter in the loop.

Benchmarks

Median per-call overhead, ReleaseFast, 100k iterations (CPython 3.14, Windows x64):

Call kind Overhead
scalar (add i64) ~90 ns
float (mul f64) ~105 ns
string (strlen) ~90 ns
multi-return (divmod) ~124 ns
zero-copy buffer (fill) ~120 ns

Head-to-head, same native function on all three bridgesadd, strlen, and fill are compiled once into a shared library and called through each binding, so the delta is pure call overhead:

Call kind Nano-FFI ctypes cffi
scalar (add i64) ~90 ns ~440 ns ~225 ns
string (strlen) ~90 ns ~300 ns ~220 ns
zero-copy buffer (fill) ~120 ns ~590 ns ~240 ns

A scalar Nano-FFI call carries roughly 5× less overhead than ctypes and 2.5× less than cffi on the reference machine. Reproduce:

python benchmarks/benchmark.py                # per-kind overhead
python benchmarks/benchmark_vs_libraries.py   # fair head-to-head vs ctypes/cffi

Numbers are machine-dependent; the harness is the source of truth.

Build from source

Requires Zig 0.15.2 and a CPython 3.10+ with development headers.

Windows (helper auto-detects your Python headers/libs):

.\scripts\build_local.ps1

Linux / macOS:

zig build -Doptimize=ReleaseFast \
  -Dpython-include="$(python3 -c 'import sysconfig; print(sysconfig.get_path("include"))')"
cp zig-out/lib/nano_ffi.so nano_ffi.so

Run the tests:

zig build test            # pure-Zig unit tests
python tests/test_python.py   # end-to-end + benchmark

Architecture

src/
├── root.zig            # entry point; exports PyInit_nano_ffi
├── comptime_bridge.zig # comptime trampoline generator (the core)
├── registry.zig        # name -> (FnPtr, Signature)
├── python_ext.zig      # the only file that imports <Python.h>
├── allocator.zig       # Python/Zig memory boundary
└── version.zig         # single source of the version string

How the speed works: makeTrampoline uses an inline for over the signature at compile time. The compiler sees explicit typed assignments, not a loop — no branches, no runtime type checks in the hot path.

Registering your own Zig function

const bridge = @import("nano_ffi").bridge;

fn add(a: i64, b: i64) i64 { return a + b; }

const AddTrampoline = bridge.makeTrampoline(add, .{
    .args = &.{ .{ .name = "a", .typ = .i64 }, .{ .name = "b", .typ = .i64 } },
    .ret  = .i64,
});

// Register at init time:
try my_registry.register("add", AddTrampoline.asPtr(), AddTrampoline.sig);

Roadmap

Nano-FFI is at 1.0 — the public API (call, version, list_functions, signature), the supported type names, and the exception mapping are frozen and follow semantic versioning. See ROADMAP.md for how it got here and CHANGELOG.md for release notes.

License

MIT — see LICENSE. Built with Zig and the CPython C-API.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

nano_ffi-1.0.0-cp312-cp312-win_amd64.whl (98.5 kB view details)

Uploaded CPython 3.12Windows x86-64

nano_ffi-1.0.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl (54.9 kB view details)

Uploaded CPython 3.12manylinux: glibc 2.28+ x86-64manylinux: glibc 2.5+ x86-64

nano_ffi-1.0.0-cp312-cp312-macosx_13_0_x86_64.whl (17.6 kB view details)

Uploaded CPython 3.12macOS 13.0+ x86-64

nano_ffi-1.0.0-cp312-cp312-macosx_13_0_arm64.whl (19.1 kB view details)

Uploaded CPython 3.12macOS 13.0+ ARM64

File details

Details for the file nano_ffi-1.0.0-cp312-cp312-win_amd64.whl.

File metadata

  • Download URL: nano_ffi-1.0.0-cp312-cp312-win_amd64.whl
  • Upload date:
  • Size: 98.5 kB
  • Tags: CPython 3.12, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nano_ffi-1.0.0-cp312-cp312-win_amd64.whl
Algorithm Hash digest
SHA256 75cc9d1205ba70123d89fc75fe5929aadecf96c85b3a976029dda87b5247b5d1
MD5 6cab6fe277103f5b2b09f408b86fc082
BLAKE2b-256 8685e27a54a157ec68125357a39f9be98e22de51dd1cedf8787b82552c2094af

See more details on using hashes here.

File details

Details for the file nano_ffi-1.0.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl.

File metadata

File hashes

Hashes for nano_ffi-1.0.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
Algorithm Hash digest
SHA256 cdbda55a5979882c2a40543d556f0d706945fc38c248a170a03ebb84ab2a553f
MD5 60fba9de5ca1aab3a94bf8663de34010
BLAKE2b-256 2c130c61dfbee5b7c6bc3b84cab56e53073ed851487f96b95306f29c6de6f5e0

See more details on using hashes here.

File details

Details for the file nano_ffi-1.0.0-cp312-cp312-macosx_13_0_x86_64.whl.

File metadata

File hashes

Hashes for nano_ffi-1.0.0-cp312-cp312-macosx_13_0_x86_64.whl
Algorithm Hash digest
SHA256 293cf8ccb01773ae6f85bbced67c286ad8c66c47934553b9024c6b6d759e1a6d
MD5 d2f7db8f7ddedd991bf9bfdaed1636b5
BLAKE2b-256 cacbfc3759b7218a7aa0b5b438e31aadceb0c4437431b36fd92191ffa0878fad

See more details on using hashes here.

File details

Details for the file nano_ffi-1.0.0-cp312-cp312-macosx_13_0_arm64.whl.

File metadata

File hashes

Hashes for nano_ffi-1.0.0-cp312-cp312-macosx_13_0_arm64.whl
Algorithm Hash digest
SHA256 515def1d04caff252945b2e7646f17d3d05df7203740a5af4d5a3f3e3f23c5f3
MD5 3ff022bb761f1e5867feafebb8ccd300
BLAKE2b-256 ba3b77f7984869a3f495ceccb8c53813a9ecb7266af79c412cabe43042c50309

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page