Skip to content

hashsig-glue: replace Box allocs with Zig-owned placement-init pattern - #934

Closed
zclawz wants to merge 1 commit into
devnet-5-spec-implfrom
feat/ffi-zero-alloc-placement
Closed

hashsig-glue: replace Box allocs with Zig-owned placement-init pattern#934
zclawz wants to merge 1 commit into
devnet-5-spec-implfrom
feat/ffi-zero-alloc-placement

Conversation

@zclawz

@zclawz zclawz commented May 26, 2026

Copy link
Copy Markdown
Contributor

Addresses the review comment on #918 requesting that key-generation and signing be refactored to avoid additional Rust heap allocations, consistent with the caller-supplies-buffer pattern already used in multisig-glue.

What changed

rust/hashsig-glue/src/lib.rs

Replaced the Box::new(T) + Box::into_raw return / Box::from_raw drop pattern with a placement-init approach:

Layout queries (new):

  • hashsig_sizeof_keypair / _signature / _public_key — Zig queries at runtime to know how much to allocate
  • hashsig_alignof_keypair / _signature / _public_key — required alignment (C malloc always satisfies this)

Placement-init (replaces Box-returning functions):

  • hashsig_keypair_generate_into(out: *mut KeyPair, ...) — writes via std::ptr::write, returns i32
  • hashsig_keypair_from_ssz_into, hashsig_sign_into, hashsig_signature_from_ssz_into, hashsig_public_key_from_ssz_into — same pattern

Drop-in-place (replaces Box::from_raw frees):

  • hashsig_keypair_deinit / hashsig_signature_deinit / hashsig_public_key_deinit — call std::ptr::drop_in_place, do NOT free the buffer (caller owns it)

Removed: hashsig_keypair_generate, hashsig_keypair_from_ssz, hashsig_keypair_free, hashsig_sign, hashsig_signature_free, hashsig_signature_from_ssz, hashsig_public_key_from_ssz, hashsig_public_key_free

pkgs/xmss/src/hashsig.zig

  • KeyPair, Signature, PublicKey wrappers now own their storage via cAlloc/cFree (thin wrappers over std.c.malloc/std.c.free)
  • C malloc always returns memory aligned to max_align_t (>=8 bytes on LP64), satisfying alignment of any Rust #[repr(C)] type in hashsig-glue
  • Public API is unchanged — same generate, fromSsz, sign, verify, toBytes, deinit methods; callers in aggregation.zig unaffected
  • PublicKeyCache slot semantics unchanged; deinit correctly reconstructs _buf + handle from the stored integer pointer

What this does NOT eliminate

  • Inner leansig Vec<u8> data inside XmssPublicKey / XmssSignature / XmssPrivateKey — inherent to the leansig API; freed correctly when drop_in_place runs Drop
  • The intermediate Vec<u8> in to_bytes() serialisation helpers — leansig API constraint; short-lived temporaries that do not escape the FFI call
  • multisig-glue — already used the caller-supplies-buffer pattern for proof bytes; no changes needed

Verification

  • cargo check -p hashsig-glue passes cleanly
  • zig ast-check on hashsig.zig, aggregation.zig, lib.zig all pass
  • No remaining callers of the removed Box-based FFI symbols in the Zig codebase

cc @GrapeBaBa

Rust no longer heap-allocates the outer KeyPair / Signature / PublicKey
wrapper structs.  Instead, each init function accepts a caller-supplied
buffer and writes into it with std::ptr::write; the matching _deinit runs
std::ptr::drop_in_place (Rust Drop) without freeing the buffer.

Zig side allocates the buffers via C malloc (always malloc-aligned, safe
for all Rust #[repr(C)] types) and frees them after calling _deinit.

New Rust exports
  hashsig_sizeof_keypair / _signature / _public_key   — allocation size
  hashsig_alignof_keypair / _signature / _public_key  — required alignment
  hashsig_keypair_generate_into  (was: hashsig_keypair_generate → *mut Box)
  hashsig_keypair_from_ssz_into  (was: hashsig_keypair_from_ssz → *mut Box)
  hashsig_keypair_deinit         (was: hashsig_keypair_free → Box::from_raw)
  hashsig_sign_into              (was: hashsig_sign → *mut Box)
  hashsig_signature_deinit       (was: hashsig_signature_free)
  hashsig_signature_from_ssz_into
  hashsig_public_key_from_ssz_into
  hashsig_public_key_deinit

Removed Rust exports (were Box-allocating):
  hashsig_keypair_generate, hashsig_keypair_from_ssz, hashsig_keypair_free
  hashsig_sign, hashsig_signature_free, hashsig_signature_from_ssz
  hashsig_public_key_from_ssz, hashsig_public_key_free

Note: the inner XMSS types (leansig XmssPublicKey / XmssSignature etc.)
still carry their own heap-allocated data (Vec<u8> inside leansig);
those are inherent to the leansig API and are freed correctly when
_deinit runs Drop in-place.  The intermediate Vec<u8> in to_bytes()
serialisation helpers is similarly a leansig API constraint; those are
short-lived temporaries that do not escape the FFI call.

Addresses: #918 (comment)
@zclawz

zclawz commented May 26, 2026

Copy link
Copy Markdown
Contributor Author

Closing — superseded by #935 which targets main as requested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants