Rust FFI Client

goldy-ffi-client is a Rust crate that loads the goldy-ffi native library at runtime and exposes the same RAII API as the core goldy crate. Instead of statically linking the goldy library, it calls the stable C ABI through libloading (LoadLibrary on Windows, dlopen on Unix).

This is the same native boundary used by the C++ and .NET bindings. Python is different — it links the core goldy crate directly via PyO3.

When to Use

Use caseCrate
Normal Rust applicationsgoldy (static link, published on crates.io)
FFI integration testsgoldy-ffi-client
Validating the C ABI from Rustgoldy-ffi-client
Swapping the native library without recompiling the clientgoldy-ffi-client

The ffi-client API mirrors the core Rust crate: Instance, Scheme, RetainedPool, MemoryExchange, SurfaceExchange, and the rest of the Fondaco programming model are available with the same names and patterns.

Installation

goldy-ffi-client is a workspace crate — it is not published to crates.io. Add it as a path dependency:

[dependencies]
goldy-ffi-client = { path = "../ffi-client" }

Build the native library first:

cargo build -p goldy-ffi

Then build or run ffi-client examples:

cd ffi-client
cargo run --example triangle_headless

Requirements

  • Rust 2021 edition
  • A built goldy_ffi shared library (goldy_ffi.dll / libgoldy_ffi.so / libgoldy_ffi.dylib)
  • A GPU with Vulkan 1.4+, DX12, or Metal Tier 2+ support (CUDA and WebGPU backends are in progress; Tenstorrent is planned)

Library Discovery

At runtime, ffi-client searches for the native library in this order:

  1. GOLDY_FFI_PATH — full path to the goldy_ffi dylib
  2. GOLDY_FFI_LIB_DIR — compile-time directory from the goldy-ffi build
  3. The directory containing the running executable

On Windows, ffi-client also calls SetDllDirectoryW so Slang DLLs next to goldy_ffi.dll are found.

# Point at a specific build of the native library
GOLDY_FFI_PATH=/path/to/libgoldy_ffi.so cargo run --example triangle_headless

Quick Start

Headless Rendering

use goldy_ffi_client::{
    shader::builtins, BufferKind, Color, Context, DeviceDescriptor, Instance, NodeAccess,
    RenderPipeline, RenderPipelineDesc, RequestAdapterOptions, RetainedPool, Scheme,
    ShaderModule, TargetLoad, TextureFlags, TextureFormat, TextureKind, Vertex2D,
};

fn main() -> goldy_ffi_client::Result<()> {
    let instance = Instance::new()?;
    let device = instance
        .request_adapter(&RequestAdapterOptions::default())?
        .request_device(&DeviceDescriptor::default())?;
    let ctx = Context::new(&device)?;

    let vertices = [
        Vertex2D { position: [0.0, -0.5], color: [1.0, 0.0, 0.0, 1.0] },
        Vertex2D { position: [-0.5, 0.5], color: [0.0, 1.0, 0.0, 1.0] },
        Vertex2D { position: [0.5, 0.5], color: [0.0, 0.0, 1.0, 1.0] },
    ];
    let mut pool = RetainedPool::new(&device)?;
    let vertex_buffer = pool.acquire_buffer_with_data(&vertices, BufferKind::Scattered)?;

    let readback = pool.acquire_texture(
        64, 64, TextureFormat::Rgba8Unorm, TextureKind::Direct,
        TextureFlags::COPY_SRC.union(TextureFlags::COPY_DST), None,
    )?;

    let shader = ShaderModule::from_slang(&device, builtins::VERTEX_COLOR_2D)?;
    let pipeline = RenderPipeline::new(
        &device, &shader, &shader,
        &RenderPipelineDesc {
            vertex_layout: Vertex2D::layout(),
            target_format: TextureFormat::Rgba8Unorm,
            ..Default::default()
        },
    )?;

    let mut scheme = Scheme::new(&ctx)?;
    let rt = scheme.lease_render_target(64, 64, TextureFormat::Rgba8Unorm, None)?;
    {
        let mut pass = scheme.render_pass("triangle", &rt, TargetLoad::Clear(Color::BLACK));
        pass.with_buffer(&vertex_buffer, NodeAccess::Read);
        pass.set_pipeline(&pipeline);
        pass.set_vertex_buffer(0, &vertex_buffer);
        pass.draw(0..3, 0..1);
        pass.finish_recorded();
    }
    scheme.copy_to_texture(&rt, &readback)?;

    let memory = goldy_ffi_client::MemoryExchange::new(&ctx)?;
    let withdraw = memory.bind_withdraw_texture(&mut scheme, &readback)?;
    let mut submission = scheme.submit()?;
    let pixels = withdraw.claim(&mut submission)?.consume()?;

    println!("Rendered {} bytes", pixels.len());
    Ok(())
}

See ffi-client/examples/triangle_headless.rs for the full example.

Windowed Rendering

See ffi-client/examples/triangle.rs and ffi-client/examples/game_of_life.rs (winit).

Compute

#![allow(unused)]
fn main() {
use goldy_ffi_client::{ComputePipeline, Context, Instance, MemoryExchange, NodeAccess, Scheme, ShaderModule};

let mut scheme = Scheme::new(&ctx)?;
let mut node = scheme.compute_node("double", &pipeline);
node.with_buffer(&buf, NodeAccess::ReadWrite);
node.dispatch(1, 1, 1);

let memory = MemoryExchange::new(&ctx)?;
let withdraw = memory.bind_withdraw(&mut scheme, &buf.field(0)?)?;
let mut submission = scheme.submit()?;
let bytes = withdraw.claim(&mut submission)?.consume()?;
}

See ffi-client/examples/compute_simple.rs.

Resource Management

All ffi-client types use RAII via Drop. Errors are returned as goldy_ffi_client::Result<T> with GoldyError — the same pattern as the core goldy crate.

Key Differences from Core goldy

Aspectgoldygoldy-ffi-client
LinkingStatic (compiled into your binary)Dynamic (libloading at runtime)
Distributioncrates.ioWorkspace path dependency
API surfaceReference implementationMirrors core API over C ABI
Native libraryEmbedded in your binarySeparate goldy_ffi dylib required
Crate namegoldygoldy_ffi_client

Functionally, application code looks nearly identical. The main difference is build and deployment: ffi-client binaries need the goldy_ffi shared library (and Slang DLLs) available at runtime.

Backend Selection

GOLDY_BACKEND works the same as with the core crate — set it before creating an Instance:

GOLDY_BACKEND=vulkan cargo run --example triangle_headless

When building goldy-ffi, pass backend features through:

cargo build -p goldy-ffi --no-default-features --features vulkan

Examples

ExampleDescription
ffi-client/examples/triangle_headless.rsOffscreen triangle + readback
ffi-client/examples/triangle.rsWindowed triangle (winit)
ffi-client/examples/compute_simple.rsCompute dispatch
ffi-client/examples/game_of_life.rsHybrid compute + render (windowed)
ffi-client/examples/game_of_life_headless.rsGame of Life readback