Skip to content

C ABI: expose eth.zig as a linkable native library (eth.h) #118

Description

@koko1123

Motivation

eth.zig is measurably the fastest Ethereum library on most of the surface we benchmark (18/26 vs alloy.rs), but today that speed is only reachable from Zig. Zig is a small pool; the people who would benefit most from a 24x faster RLP encoder or a 4.23x faster sign+recover are the Python, Node, and Go teams running signing and decoding in hot loops, and they will never write Zig.

A stable C ABI turns eth.zig from "a Zig library" into "the fast native core other ecosystems bind to" -- the same path that made libsecp256k1, blst, and rust cryptography ubiquitous. This is the single highest-leverage adoption change available to the project, and Zig's C interop makes it nearly free: export fn plus a hand-written header.

Scope

A new src/c_api.zig plus a generated include/eth.h, exposing a deliberately small, allocation-free-where-possible surface:

  • Hashing / primitives: eth_keccak256(const uint8_t *data, size_t len, uint8_t out[32]), eth_address_from_pubkey, eth_address_checksum(const uint8_t addr[20], char out[43])
  • secp256k1: eth_sign(const uint8_t key[32], const uint8_t hash[32], uint8_t out_sig[65]), eth_recover(const uint8_t sig[65], const uint8_t hash[32], uint8_t out_addr[20])
  • Transactions: eth_tx_sign(...) over a C struct mirroring transaction.Eip1559Transaction, returning the signed RLP into a caller-provided buffer plus a written length; eth_tx_hash(...)
  • ABI: eth_abi_selector(const char *signature, uint8_t out[4]), and encode/decode entry points over a caller-provided buffer
  • RLP: eth_rlp_encode / eth_rlp_decode over caller buffers

Design rules that keep this maintainable:

  • Caller-allocates. Every function writes into a caller-provided buffer and returns a written length or a negative error code. No eth_free, no ownership questions across the FFI boundary, no allocator smuggled through an opaque handle. Provide eth_*_max_len helpers so callers can size buffers.
  • A single flat error enum (typedef enum { ETH_OK = 0, ETH_ERR_BUFFER_TOO_SMALL = -1, ... }) mapped from Zig error sets in one place, so the mapping is auditable.
  • Network calls stay out of v1. Signing, hashing, encoding, decoding only -- the pure-compute paths where we actually win. Transports drag in std.Io, TLS, and lifetime questions that would triple the surface for a fraction of the value. Bindings can do their own HTTP.
  • No breaking changes without a soname bump. The header is a public contract from the day someone links it.

Deliverables:

  • zig build c-lib producing libethzig.a and libethzig.so/.dylib for the host target, plus include/eth.h
  • A round-trip test suite driven from C (compiled in CI) asserting the C surface matches the Zig one on the same vectors
  • A examples/ffi/ directory with a minimal C program and one Python ctypes script that signs a transaction, so the README can show the payoff in ten lines
  • CI job cross-compiling the shared library for linux-x86_64, linux-aarch64, macos-aarch64

Follow-ups (deliberately not in this issue)

Published bindings packages (PyPI / npm) belong in their own repos and their own issues once the ABI is stable. Same for a cgo wrapper.

Pointers

src/root.zig is the module map; the functions worth exporting first are the ones in bench/RESULTS.md where we win by the widest margin. build.zig currently has no b.option calls and unconditionally sets link_libc = true, so adding a library artifact is additive. Keep src/c_api.zig free of any logic -- it should be a pure translation layer over the existing modules, so the Zig API stays the source of truth.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions