Language bindings¶
asm-test ships bindings for ten languages so you can drive the framework from your own test suite. Every binding exposes the same core capabilities:
the capture trampoline — run a routine through the real ABI and snapshot registers, flags, and the FP/vector return lanes;
the emulator — run it in a virtual CPU, where faults are data, not a crash;
an optional in-line assembler (Keystone) — pass a routine as assembly text instead of a compiled address, then either run it in the emulator or just assemble it to machine-code bytes (multi-arch); and
an optional disassembler (Capstone) —
disas(bytes, off)decodes one instruction back to"mnemonic operands"text (e.g. to name the instruction at an emulator fault’srip).
Both extra tiers now ship inside libasmtest_emu itself — it is the full
superset (emulator + Keystone assembler + Capstone disassembler), so they are on
by default (see The optional native tiers below).
Three further tiers — in-process DynamoRIO (libasmtest_drapp),
the hardware / single-step tier (libasmtest_hwtrace), and the
data-flow tier (libasmtest_dataflow) — are separate,
advanced opt-ins (none is part of the superset). Every binding ships a
wrapper for all three: a drtrace (NativeTrace), an hwtrace (HwTrace),
and a dataflow (value-trace L0→L1→L2) module,
each dlopen-loading its library at run time and self-skipping when it is absent. The
single-step backend in particular traces live on any x86-64 Linux or macOS with no engine
install, so each language page works it end to end. See
Native runtime tracing and
Data-flow tracing. A binding still keeps an
asm_available / disas_available probe so it can self-skip if pointed at an
older/leaner lib that lacks them.
This page is the shared overview — architecture, one-time setup, and the capability map common to every binding. Each language then has its own page with an end-to-end, idiomatic example:
Language |
Page |
FFI mechanism |
Minimum version targeted |
|---|---|---|---|
Python |
|
Python 3.8 ( |
|
.NET |
P/Invoke |
.NET 8.0 ( |
|
Go |
|
Go 1.21 ( |
|
Rust |
|
Rust 1.70+ (2021 edition; |
|
C++ |
direct |
C++17 ( |
|
Zig |
|
Zig 0.13.0 ( |
|
Node.js |
|
Node.js 18 ( |
|
Java |
FFM (Panama) |
JDK 22 ( |
|
Ruby |
|
Ruby 2.6 ( |
|
Lua |
LuaJIT |
LuaJIT (Lua 5.1 ABI) |
The minimum version column is the floor each binding actually builds and tests
against — taken from its packaging descriptor (pyproject.toml, go.mod, the
.csproj/Cargo.toml/pom.xml/.gemspec/.rockspec, etc.), not an aspiration.
Newer toolchains work; older ones are unsupported.
Python is the reference binding (start there if you’re new); .NET and Go are the other two with worked, all-three-capability examples. For how each package is assembled and published, see Packaging the bindings.
Every binding loads the shared library built from this repo and calls the
binding ABI — the macro-free entry points catalogued in the
API reference. The Python binding reads struct layout from
the asmtest_abi.json manifest; the rest go through the opaque-handle accessors
(asmtest_regs_*, asmtest_emu_*), so no regs_t layout is mirrored on their
side.
The whole substrate hangs off one flat C-ABI surface, kept faithful by the layout manifest and a shared conformance corpus — the single source of truth every binding must reproduce:
Diagram: Language bindings architecture
One-time setup¶
From the repository root, build the native library the bindings load:
make shared-emu # libasmtest_emu.{so,dylib} — capture trampoline + emulator + FFI accessors
make manifest # asmtest_abi.json — required by the Python binding only
The optional native tiers¶
The in-line assembler (Keystone) and the disassembler (Capstone) are folded into
libasmtest_emu itself, which is the full superset (emulator + Keystone +
Capstone). make shared-emu builds it with all three tiers, so every binding gets
them with no extra flag. The language packages built by make <lang>-package
bundle this lib and vendor the native deps, so a package install has both tiers
working with no system libs — but note the packages are not on public
registries yet (see Maturity): today you consume the bindings
from a checkout as shown below and on each language’s page (see
Packaging the bindings for the package pipeline).
libasmtest_emu is the one lib carrying both optional tiers (and what the
packages ship): load it and you get the assembler and the disassembler from a
single library (no combinatorial lib matrix).
Your routine under test is any System V ABI function in a shared library.
Assemble yours with the
asm.h shim into
one:
cc -shared -fPIC -Iinclude -o libmyroutines.so myroutines.s
At run time each binding must find libasmtest_emu. The mechanism differs per
language — this is the complete setup cheatsheet (each language page repeats its
own row with context):
Language |
Point it at the built libs |
|---|---|
Python |
auto-discovers |
Node / Ruby / Lua |
|
Java |
|
.NET / Go |
|
Rust |
|
C++ |
compile/link directly: |
Zig |
|
The emulator tier additionally needs libunicorn, and the assembler/disassembler tiers need libkeystone + libcapstone (see Emulator tier).
Because libasmtest_emu carries both optional tiers, every binding gets the
assembler and the disassembler from a single load — make <lang>-test exercises
both. The dynamic-FFI bindings still keep runtime asm_available() /
disas_available() (or AsmAvailable / DisasAvailable) probes as defensive
checks — if a consumer points ASMTEST_LIB at an older/leaner lib without these
tiers, those calls
self-skip. Of the statically compiled bindings, Zig compiles the tiers in by
default, whereas the C++ binding gates the emulator/assembler/disassembler tiers
behind opt-in defines — -DASMTEST_ENABLE_EMU / -DASMTEST_ENABLE_ASM /
-DASMTEST_ENABLE_DISAS — so without them asmtest.hpp declares only the core
surface.
Capabilities at a glance¶
Every binding exposes the same surface. Here each capability is mapped to the three featured bindings’ idiom; the other seven mirror these (see each language’s page, linked above).
Capability |
Python |
.NET |
Go |
|---|---|---|---|
Resolve a built-in corpus routine |
load your own lib ( |
|
|
Integer capture (≤6 args) |
|
|
|
Wide-arity capture (stack args) |
|
|
|
Float/double capture |
|
|
|
Mixed integer+FP capture |
|
|
|
Struct return (hidden pointer) |
|
|
|
Vector / SIMD capture |
|
|
|
Integer return value |
|
|
|
FP return value |
|
|
|
Vector return lanes |
|
|
|
Condition flag (CF/ZF/…) |
|
|
|
ABI (callee-saved) preserved |
|
|
|
Run under the emulator |
|
|
|
Fault is data, not a crash |
|
|
|
Where/why a fault hit |
|
|
|
Read a guest register (GP + |
|
|
|
Read a guest XMM lane |
|
|
|
Emulate raw bytes (≤6 int args) |
|
|
|
Emulate with FP / vector args |
|
|
|
Win64 calling convention |
|
|
|
Cross-arch guest (arm64/riscv/arm) |
|
|
|
Read a cross-arch guest register |
|
|
|
Execution trace / block coverage |
|
|
|
Memory-write watchpoint (Track F) |
|
|
|
Register invariant (Track F) |
|
|
|
Coverage-guided fuzzing (Track E) |
|
|
|
Mutation testing (Track E) |
|
|
|
AVX2 256-bit capture (Track D) |
|
|
|
In-line assembler present? |
|
|
|
Run assembly text |
|
|
|
Assemble text → bytes (multi-arch) |
|
|
|
Disassembler present? |
|
|
|
Disassemble bytes → text (Track C) |
|
|
|
Nine of the ten bindings also ship Tier-2 assertions over these results —
assert_ret, assert_abi_preserved, assert_flag, assert_fp,
assert_vec_f32, assert_no_fault, assert_fault, assert_reg, and friends
(Asm.Assert.* in .NET, asmtest.Assert* in Go). Zig is the exception: it
consumes the C headers directly and a consumable assertion layer is still future
work (see its page). Each language’s page works the core tiers
end-to-end — capture, the emulator, cross-arch guests, and the in-line assembler.
The newer additions above (AVX2 256-bit capture, mid-execution guards,
coverage-guided fuzzing / mutation testing, and the disassembler) are mapped in
this table and catalogued in the API reference; the
Python reference page documents each in full.
Almost every binding has a reusable module¶
Nine of the ten bindings expose a reusable library module that keeps the FFI
inside and presents an idiomatic surface — capture/emulator handles, the optional
in-line assembler, plus Tier-2 assertions — with a thin conformance runner
consuming it (the same corpus, in each language), so they require no FFI
declarations in your own test code. Zig is the exception: core usage is raw
@cImport of the C headers (only its native-trace tier wrappers ship as src/
modules), so expect C-shaped calls there:
Language |
Module |
FFI mechanism |
Consumer |
|---|---|---|---|
Python |
|
|
|
Go |
|
|
|
Rust |
|
|
|
C++ |
|
direct |
|
Zig |
raw |
|
build step |
Node |
|
|
|
Ruby |
|
|
|
Lua |
|
LuaJIT |
|
Java |
|
FFM (Panama) |
|
.NET |
|
P/Invoke |
|
Every module deliberately avoids mirroring regs_t: Python reads field offsets
from the layout manifest, and the rest call the opaque-handle accessors. That
binding-ABI surface — the array-form capture entry points, the verdict shims, and
the opaque-handle accessors — is catalogued in the
API reference.
Maturity¶
Python is the reference binding: packaged (pyproject.toml / wheel), pytest
fixtures, and both tiers — the most turnkey today. The others ship the same
reusable module and Tier-2 assertions, and their packages now build, bundle the
native lib, and install + smoke-test in the release dry-run (Linux + macOS) —
they are simply not published to registries yet (only the credentialed
go-live remains; see Packaging the bindings). Today you consume
them as shown on each language’s page (referencing the module and pointing at the
built shared libs) — exactly how the repo wires make <lang>-test.