Embedding Nexium in Python and C

The seam is the product (spec section 4). This is the afternoon: write one function, ship it, call it from the project you already have.

1. Write and export#

pub fn checksum(data: []u8) -> u32 export(c) {
    var h: u32 = 2166136261
    for b in data {
        h ^= b as u32
        h *%= 16777619
    }
    return h
}

pub fn dot(a: []f64, b: []f64) -> !f64 export(c) {
    if a.len != b.len { return error.InvalidInput }
    var acc: f64 = 0.0
    for x, y in a, b { acc += x * y }
    return acc
}

artifact cabi   { name = "ropesim" }
artifact python { name = "ropesim", version = "0.1.0" }

Exportable parameter types: integers, floats, bool, char, slices of those ([]f64 arrives as a pointer and a length), and layout(c) structs of scalars. Return types: scalars, layout(c) structs, void, or an error union of one of those. Slices cannot be returned (their lifetime cannot be expressed across the boundary); write into a caller-provided []mut T instead.

2. Ship#

nx ship ropesim.nx                 # host target
nx ship ropesim.nx --target x86_64-linux-gnu

Output in nx-out/ropesim/:

filepurpose
ropesim.hC header with one prototype per export
ropesim.dll / libropesim.so / libropesim.dylibshared library
ropesim.lib / libropesim.astatic library
ropesim.cthe generated C, for inspection
python/ropesim/importable package (__init__.py, .pyi stubs, the shared library)
ropesim-0.1.0-py3-none-<platform>.whlinstallable wheel

3. The C ABI#

The effect signature decides the shape (S3: a panic never crosses the boundary):

/* effects: none; cannot panic, so it returns its value directly */
uint32_t checksum(const uint8_t* data, size_t data_len);

/* effects: none */
int32_t dot(const double* a, size_t a_len, const double* b, size_t b_len, double* out);

/* effects: panics */
int32_t divide(int64_t a, int64_t b, int64_t* out);

const char* ropesim_error_name(int32_t code);
const char* ropesim_last_panic(void);

Link statically (ropesim.lib / libropesim.a) or load the shared library. The static library has no runtime dependency beyond libc and libm.

4. From Python#

import array, ropesim

ropesim.checksum(b"hello")                    # 1335831723
pos = array.array("d", [0.1, 0.2, 0.3])
ropesim.simulate(pos, 0.01, 100)              # []mut f64 writes back into `pos`
ropesim.dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]) # 32.0

try:
    ropesim.dot([1.0], [1.0, 2.0])
except ropesim.NexiumError as e:
    print(e.name, e.code)                     # InvalidInput 9

try:
    ropesim.divide(1, 0)
except ropesim.NexiumPanic as e:
    print(e.message)                          # division by zero (at ropesim.nx:31)

The package uses ctypes over the shared library: one wheel per platform, any Python 3.8+ interpreter (the "stable ABI" default of spec 4.2). Slice arguments accept bytes, bytearray, array.array, memoryview, or any sequence; a []mut T parameter needs a writable buffer (bytearray or array.array) and writes through to it. layout(c) structs become ctypes.Structure subclasses with the same field names. Type stubs (__init__.pyi) are included for editors.

Install the wheel with pip install nx-out/ropesim/ropesim-0.1.0-py3-none-*.whl, or add nx-out/ropesim/python to sys.path during development.

5. From Rust#

Declare artifact rustlib { name = "ropesim" } and nx ship writes a crate to nx-out/ropesim/rust/ with the static archive in lib/, a build.rs that links it, extern "C" declarations, #[repr(C)] structs, and safe wrappers:

// Cargo.toml: ropesim = { path = "nx-out/ropesim/rust" }
let mut pos = [0.1, 0.2, 0.3];
ropesim::simulate(&mut pos, 0.01, 100);          // &mut [f64] -> pointer and length
ropesim::dot(&[1.0, 2.0], &[3.0, 4.0]);          // Result<f64, NexiumError>
ropesim::divide(1, 0);                           // Err(NexiumError::Panic { message })

Slices become &[T] / &mut [T], status-returning exports become Result<T, NexiumError> with Error { code, name } and Panic { message } variants, and !panics exports are plain functions. On Windows the archive is compiled for the MSVC ABI so it links into the default Rust toolchain.