# ffi-bridge
> Memory-safe Go↔Rust FFI boundary helpers: buffer management, error propagation, panic safety, and named callback registration.
[](https://crates.io/crates/ffi-bridge)
[](https://pkg.go.dev/github.com/ChainPrimitives/ffi-bridge)
[](https://opensource.org/licenses/MIT)
[](https://github.com/ChainPrimitives/ffi-bridge)
[](https://github.com/ChainPrimitives/ffi-bridge/pulls)
A dual-language package — a **Go module** and a **Rust crate** — that provides safe abstractions for crossing the Go↔Rust FFI boundary. Extracted from a hybrid-runtime blockchain engine where correctness at the FFI layer is critical.
---
## Why ffi-bridge?
FFI between Go and Rust is notoriously error-prone:
| Dangling pointers | Explicit ownership: Rust allocates, Rust frees via exported functions |
| GC interference | `runtime.SetFinalizer` isolates Go GC from Rust heap allocations |
| Panic unwinds crossing ABI | Every `extern "C"` fn wraps its body in `catch_panic` |
| Error propagation loss | `FfiResult` carries typed error codes + messages over the boundary |
| Callback lifetime bugs | Named callback registry with `Mutex`-guard and `once_cell` |
---
## Repository Layout
```
ffi-bridge/
├── go/ # Go module (github.com/ChainPrimitives/ffi-bridge)
│ ├── go.mod
│ ├── bridge.go # Buffer type + FromBytes / FromJSON
│ ├── memory.go # Low-level alloc helpers, FfiString conversion
│ ├── errors.go # ErrorCode, FfiError, CheckResult
│ ├── types.go # Type mirrors + Version()
│ ├── callback.go # InvokeCallback / InvokeCallbackJSON
│ ├── bridge_test.go # Unit tests + benchmarks
│ └── examples/
│ └── basic/main.go
├── rust/ # Rust crate (ffi-bridge on crates.io)
│ ├── Cargo.toml
│ ├── src/
│ │ ├── lib.rs
│ │ ├── memory.rs # FfiBuffer, FfiString
│ │ ├── errors.rs # FfiErrorCode, FfiError, FfiResult, catch_panic
│ │ ├── types.rs # BridgeValue, utilities
│ │ ├── bridge.rs # BridgeCall, ffi_echo, ffi_version
│ │ └── callback.rs # Named callback registry
│ ├── tests/
│ │ └── integration.rs
│ └── examples/
│ └── basic.rs
├── shared/
│ ├── ffi.h # C header — the ABI contract
│ └── error_codes.h # Standalone error code definitions
├── Makefile
├── LICENSE
└── README.md
```
---
## Prerequisites
| Rust | ≥ 1.70 | Build the Rust crate |
| Go | ≥ 1.21 | Build the Go module |
| C compiler | Any (clang/gcc) | CGo linkage |
| `cargo` | Latest stable | Rust build system |
---
## Getting Started
### 1. Clone and build
```bash
git clone https://github.com/ChainPrimitives/ffi-bridge
cd ffi-bridge
make # Builds Rust (release) then Go
make test # Runs all tests
make example # Runs the Rust example
```
### 2. Use the Rust crate
Add to `Cargo.toml`:
```toml
[dependencies]
ffi-bridge = "1.0"
```
### 3. Use the Go module
```bash
go get github.com/ChainPrimitives/ffi-bridge
```
> **Note:** CGO_ENABLED=1 is required. The Rust shared library must be built
> and accessible via `LD_LIBRARY_PATH` (Linux) or `DYLD_LIBRARY_PATH` (macOS)
> at link time.
---
## Rust API
### Buffer allocation
```rust
use ffi_bridge::{FfiBuffer, ffi_buffer_free};
// Allocate a buffer
let buf = FfiBuffer::new(1024);
// From a Vec<u8> (zero-copy, transfers ownership)
let buf = FfiBuffer::from_vec(vec![1, 2, 3]);
// From a JSON-serializable value
let buf = FfiBuffer::from_json(&my_struct)?;
// Read as a slice
let slice = unsafe { buf.as_slice() };
// Deserialize
let value: MyType = unsafe { buf.to_json() }?;
// Free (must be called when done — no Drop)
ffi_buffer_free(buf);
```
### FfiResult
```rust
use ffi_bridge::{FfiResult, FfiError, catch_panic};
// Constructors
let ok_result = FfiResult::ok(some_buffer);
let err_result = FfiResult::err(FfiError::Timeout);
// Panic-safe wrapper
let result = catch_panic(|| {
// ... potentially panicking code
Ok(FfiBuffer::from_vec(b"output".to_vec()))
});
```
### Callbacks
```rust
use ffi_bridge::{register_callback, FfiResult, FfiBuffer};
// Register from Rust
register_callback("my.handler", |input| {
let req: MyRequest = unsafe { input.to_json() }?;
let resp = process(req);
match FfiBuffer::from_json(&resp) {
Ok(buf) => FfiResult::ok(buf),
Err(e) => FfiResult::err(e),
}
})?;
```
### BridgeCall
```rust
use ffi_bridge::BridgeCall;
#[no_mangle]
pub extern "C" fn my_fn(input: FfiBuffer) -> FfiResult {
BridgeCall::new(input).run_json(|req: MyRequest| {
Ok(process(req)) // MyResponse implements Serialize
})
}
```
---
## Go API
### Buffer operations
```go
import ffibridge "github.com/ChainPrimitives/ffi-bridge"
// From bytes
buf := ffibridge.FromBytes([]byte("hello rust"))
defer buf.Free()
// From a JSON-serializable value
buf, err := ffibridge.FromJSON(myStruct)
if err != nil { ... }
defer buf.Free()
// Read back
bytes := buf.Bytes()
// Decode JSON
var resp MyResponse
if err := buf.ToJSON(&resp); err != nil { ... }
```
### Invoking Rust callbacks
```go
// Raw buffer
result, err := ffibridge.InvokeCallback("my.handler", inputBuf)
if err != nil { ... }
defer result.Free()
// JSON convenience (serialize in → deserialize out)
var resp MyResponse
err := ffibridge.InvokeCallbackJSON("my.handler", myRequest, &resp)
```
### Errors
```go
// FfiError carries a typed error code + message
result, err := ffibridge.InvokeCallback("unknown.callback", buf)
if err != nil {
ffiErr := err.(*ffibridge.FfiError)
fmt.Println(ffiErr.Code) // ffibridge.ErrNotFound
fmt.Println(ffiErr.Message) // "not found: unknown.callback"
}
```
### Version check
```go
ver := ffibridge.Version() // e.g. "1.0.0"
```
---
## C Header
The shared C ABI contract lives in `shared/ffi.h`. Include it in any C, C++, or CGo consumer:
```c
#include "shared/ffi.h"
FfiBuffer buf = ffi_buffer_alloc(128);
// ... write data into buf.data, set buf.len ...
ffi_buffer_free(buf);
FfiResult result = ffi_invoke_callback("my.handler", buf);
if (result.error_code != FFI_OK) {
// handle error
}
ffi_result_free(result);
```
---
## Safety Guarantees
### Memory safety
- **Rust allocates, Rust frees.** Go never calls `malloc`/`free` for FFI buffers.
All allocation goes through Rust's global allocator.
- **GC isolation.** Go's garbage collector cannot see Rust heap memory.
`runtime.SetFinalizer` on the Go `*Buffer` wrapper ensures cleanup if
`Free()` is not called explicitly.
- **No double-free.** `Buffer.Free()` is idempotent. `CheckResult` zeroes the
payload field of `FfiResult` before calling `ffi_result_free` to prevent
the Rust side from freeing bytes that have already been copied to Go.
### Panic safety
- **Panics never cross the FFI boundary.** Every `extern "C"` function in the
Rust crate wraps its body in `catch_panic`, which uses `std::panic::catch_unwind`.
A caught panic is converted to `FfiResult { error_code: FFI_ERR_PANIC, ... }`.
### Thread safety
- **Callback registry** is protected by `std::sync::Mutex<HashMap>` initialized
with `once_cell::sync::Lazy`. Registrations from any thread are safe.
- Poisoned locks are detected and reported as `FFI_ERR_LOCK_POISONED`.
---
## Error Codes
| `FFI_OK` | 0 | Success |
| `FFI_ERR_NULL_POINTER` | 1 | Required pointer was null |
| `FFI_ERR_BUFFER_TOO_SMALL` | 2 | Buffer smaller than required |
| `FFI_ERR_INVALID_UTF8` | 3 | Input is not valid UTF-8 |
| `FFI_ERR_SERIALIZATION` | 4 | JSON serialization failed |
| `FFI_ERR_PANIC` | 5 | Rust panic caught at boundary |
| `FFI_ERR_TIMEOUT` | 6 | Operation timed out |
| `FFI_ERR_NOT_FOUND` | 7 | Named resource not found |
| `FFI_ERR_LOCK_POISONED` | 8 | Mutex lock poisoned |
| `FFI_ERR_UNKNOWN` | 99 | Unclassified error |
Error codes are **stable** — values are never renumbered or removed.
---
## Build Details
### Makefile targets
```bash
make # Build Rust (release) + Go
make rust # Rust only
make go # Go only (builds Rust first)
make test # All tests
make test-rust # Rust tests only
make test-go # Go tests only
make bench # Benchmarks (both sides)
make example # Run Rust basic example
make lint # cargo clippy + go vet
make fmt # cargo fmt + gofmt
make clean # Remove all build artifacts
make publish # cargo publish + git tag go/v*
```
### CGo link flags
The Go module uses `#cgo LDFLAGS` to locate the Rust shared library:
```
-L${SRCDIR}/../rust/target/release -lffi_bridge
```
For production deployments, install the library system-wide or set
`CGO_LDFLAGS` / `LD_LIBRARY_PATH` appropriately.
### Platform support
| Linux x86_64 | `x86_64-unknown-linux-gnu` | `.so` |
| macOS arm64 | `aarch64-apple-darwin` | `.dylib` |
| macOS x86_64 | `x86_64-apple-darwin` | `.dylib` |
---
## Testing Strategy
- **Unit tests** in each Rust module (`#[cfg(test)]`) — buffer alloc/free, JSON round-trips, error enum mapping, panic catching.
- **Integration tests** in `rust/tests/integration.rs` — full FFI surface area end-to-end.
- **Go tests** in `go/bridge_test.go` — buffer lifecycle, JSON helpers, error types, benchmarks.
- **Memory safety** — run tests under AddressSanitizer (Linux):
```bash
cd rust && RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu
```
- **Valgrind** (Linux):
```bash
valgrind --leak-check=full cd go && CGO_ENABLED=1 go test ./...
```
---
## Publishing
```bash
# Rust crate
cd rust && cargo publish
# Go module — tag follows the go/ prefix convention for multi-module repos
git tag go/v1.0.0
git push origin go/v1.0.0
```
Or use `make publish` which runs tests first.
---
## Development
```bash
git clone https://github.com/ChainPrimitives/ffi-bridge
cd ffi-bridge
make # Build everything
make test # Run all tests
make lint # Clippy + go vet
make fmt # Auto-format both sides
make example # Run the Rust basic example
```
---
## Contributing
Contributions are welcome! This project follows standard open-source practices.
### Getting Started
1. **Fork** the repository
2. **Clone** your fork:
```bash
git clone https://github.com/YOUR_USERNAME/ffi-bridge
cd ffi-bridge
make
```
3. **Branch** for your change:
```bash
git checkout -b feat/my-feature
```
4. **Test** your changes:
```bash
make lint && make test
```
5. **Commit** using [Conventional Commits](https://www.conventionalcommits.org/):
```
feat: add FfiString length validation
fix: prevent double-free in CheckResult on empty payload
docs: expand safety guarantee documentation
```
6. Open a **Pull Request** against `main`.
### Guidelines
- **Tests required.** New functionality must include unit tests.
- **No unsafe without justification.** All `unsafe` blocks must have a `// SAFETY:` comment.
- **No breaking changes** to `FfiErrorCode` values or exported C symbols.
- **Keep FFI boundary minimal.** Only `repr(C)` POD types may cross the boundary.
- For security vulnerabilities, email **subaskar.sr@gmail.com** directly.
---
## Changelog
### v1.0.0
- 🚀 Initial release
- `FfiBuffer` — explicit-ownership heap buffer with alloc/free
- `FfiString` — UTF-8 string allocation
- `FfiResult` — C-ABI result type with error code + message
- `catch_panic` — panic boundary guard
- `BridgeCall` — high-level FFI call builder with JSON helpers
- Named callback registry with thread-safe global state
- Go module: `Buffer`, `FromBytes`, `FromJSON`, `InvokeCallback`, `InvokeCallbackJSON`, `CheckResult`
- Shared C header (`shared/ffi.h`, `shared/error_codes.h`)
- Full test suite: Rust unit + integration tests, Go unit tests + benchmarks
- Makefile for build orchestration
---
## License
MIT © 2026 [Subaskar Sivakumar](https://github.com/Subaskar-S)