SPORE language bindings

Use the SPORE core from Python, Go, and JavaScript — not by reimplementing it, but by calling the same Rust library through a small C interface. The wrappers are generated, so all three languages stay in lock-step with the Rust code.

src/ffi.rs   ──►  libspore.so   ──►  Python (ctypes)
   (C ABI)      (cargo build)        Go     (cgo)
                                     JS     (koffi)

Quick start

cargo build --release            # builds target/release/libspore.{so,dylib,dll}
python3 bindings/generate.py     # (re)generate the wrappers from spec.json

Then, in each language:

Python (ctypes — no dependencies)
import sys; sys.path.insert(0, "bindings/python")
import spore

sk, pk = spore.keypair()
addr   = spore.addr_of(pk)
wire   = spore.message_new(sk, addr, 1_700_000_000, b"hello")
assert spore.message_verify(wire)
assert spore.message_payload(wire) == b"hello"

Run the smoke test: python3 bindings/python/test.py.

Go (cgo)
import spore "spore"          // bindings/go

sk, pk := spore.Keypair()
addr := spore.AddrOf(pk)
wire := spore.MessageNew(sk, addr, 1700000000, []byte("hello"))
_ = spore.MessageVerify(wire)

Run the test: cd bindings/go && LD_LIBRARY_PATH=../../target/release go test ./....

JavaScript / Node (koffi)
const s = require("./bindings/node/spore.js");   // npm install koffi

const [sk, pk] = s.keypair();
const addr = s.addr_of(pk);
const wire = s.message_new(sk, addr, 1700000000, Buffer.from("hello"));
s.message_verify(wire); // true

Run the test: cd bindings/node && npm install && node test.js. In the browser, load the wasm32 build of the same crate instead.

The wrappers all share one core, so they interoperate: a message signed in Python verifies in Go or Node unchanged.

What's exposed

The C ABI (src/ffi.rs) covers the byte-in/byte-out primitives — identity, signed messages, sealing, encrypted topics, and text armor. Everything takes and returns bytes; functions that can fail return None/nil/null.

Deep dive: the surface and how generation works

spec.json lists each function with a tiny type vocabulary (bytes, in8/16/32, out8/16/32, u32; returns void / bytes / bool). generate.py reads it and emits spore.h, python/spore.py, go/spore.go, and node/spore.js. To change the surface: edit src/ffi.rs and spec.json together, then rerun the generator. The Rust side has its own test (cargo test ffi); each wrapper has a smoke test.

Function Purpose
keypair / prekey Ed25519 signing pair / X25519 encryption prekey pair
addr_of / topic_of SPORE address / topic address (8 bytes)
message_new / message_verify build+sign a DATA envelope / check its signature
message_id / message_payload its 16-byte content ID / its payload
seal / open anonymous sealed box to/from a prekey
topic_seal / topic_open encrypted-topic pre-shared-key crypto
armor_wrap / armor_unwrap ~S1.…~ Base32 text you can paste or print

Buffers returned across the ABI are freed by the wrapper automatically (spore_bytes_free); callers never manage memory. The full router, sessions, files, and bridges stay on the Rust side — the bindings are for the portable message/crypto primitives an app in another language actually needs.