Skip to main content

FFI Interface

The FFI (Foreign Function Interface) layer enables communication between the Rust core (chameleon-core) and Go runtime (chameleon) via a stable C ABI. This boundary handles schema parsing, validation, SQL generation, and memory management across language boundaries.

Overview

The FFI bridge provides:
  1. Schema parsing - .cham → JSON AST
  2. Schema validation - Type checking with structured errors
  3. SQL generation - Queries and migrations
  4. Memory management - Safe allocation/deallocation across boundaries
  5. Version checking - Ensure Rust/Go compatibility

Architecture

┌─────────────────────────────────────────────────┐
│  Go Application                                 │
│  (chameleon/pkg/engine)                         │
└────────────────┬────────────────────────────────┘

                 │ Go FFI bindings (cgo)

┌────────────────▼────────────────────────────────┐
│  C ABI Boundary                                 │
│  - chameleon_parse_schema()                     │
│  - chameleon_validate_schema()                  │
│  - chameleon_generate_sql()                     │
│  - chameleon_generate_migration()               │
│  - chameleon_free_string()                      │
└────────────────┬────────────────────────────────┘

                 │ unsafe extern "C"

┌────────────────▼────────────────────────────────┐
│  Rust Core Library                              │
│  (chameleon-core/src/ffi/mod.rs)                │
│  - Parser (LALRPOP)                             │
│  - Type Checker                                 │
│  - SQL Generator                                │
└─────────────────────────────────────────────────┘
Data flow:
  1. Go calls C function via cgo
  2. C function marshals to Rust (UTF-8 strings)
  3. Rust processes and returns JSON (serialized via serde_json)
  4. C function returns pointer to Go
  5. Go deserializes JSON and frees Rust memory

C ABI Functions

Location: chameleon-core/src/ffi/mod.rs

Parse Schema

#[no_mangle]
pub unsafe extern "C" fn chameleon_parse_schema(
    input: *const c_char,
    error_out: *mut *mut c_char,
) -> *mut c_char
Parameters:
  • input - Null-terminated .cham source code
  • error_out - Output pointer for error JSON (if any)
Returns:
  • JSON AST on success (caller must free with chameleon_free_string)
  • NULL on failure (error_out contains error JSON)
Example:
char* error = NULL;
char* schema_json = chameleon_parse_schema(
    "entity User { id: uuid primary, }",
    &error
);

if (schema_json == NULL) {
    printf("Parse error: %s\n", error);
    chameleon_free_string(error);
} else {
    printf("Schema: %s\n", schema_json);
    chameleon_free_string(schema_json);
}

Validate Schema

#[no_mangle]
pub unsafe extern "C" fn chameleon_validate_schema(
    input: *const c_char,
    error_out: *mut *mut c_char,
) -> ChameleonResult
Parameters:
  • input - Null-terminated .cham source code
  • error_out - Output pointer for validation result JSON
Returns:
  • ChameleonResult::Ok - Schema is valid
  • ChameleonResult::ParseError - Syntax error
  • ChameleonResult::ValidationError - Type check failed
  • ChameleonResult::InternalError - Unexpected error
Result JSON (success):
{
  "valid": true,
  "errors": []
}
Result JSON (failure):
{
  "valid": false,
  "errors": [
    {
      "kind": "ValidationError",
      "message": "Entity 'User' has no primary key",
      "line": null,
      "column": null,
      "snippet": null,
      "suggestion": null
    }
  ]
}

Generate SQL

#[no_mangle]
pub unsafe extern "C" fn chameleon_generate_sql(
    query_json: *const c_char,
    schema_json: *const c_char,
    error_out: *mut *mut c_char,
) -> ChameleonResult
Parameters:
  • query_json - Query specification (JSON)
  • schema_json - Schema AST (JSON)
  • error_out - Output pointer for generated SQL (JSON)
Returns:
  • ChameleonResult::Ok - SQL generated (error_out contains result)
  • ChameleonResult::ValidationError - Invalid query
  • ChameleonResult::InternalError - Unexpected error

Generate Migration

#[no_mangle]
pub unsafe extern "C" fn chameleon_generate_migration(
    schema_json: *const c_char,
    error_out: *mut *mut c_char,
) -> ChameleonResult
Parameters:
  • schema_json - Schema AST (JSON)
  • error_out - Output pointer for migration SQL
Returns:
  • ChameleonResult::Ok - Migration SQL in error_out
  • ChameleonResult::ValidationError - Invalid schema
  • ChameleonResult::InternalError - Unexpected error

Free String

#[no_mangle]
pub unsafe extern "C" fn chameleon_free_string(s: *mut c_char)
Parameters:
  • s - String allocated by Rust (from any FFI function)
Safety:
  • Safe to call with NULL (no-op)
  • Must be called for every non-NULL pointer returned by Rust
  • Never call twice on the same pointer (double-free)

Version

#[no_mangle]
pub extern "C" fn chameleon_version() -> *const c_char
Returns:
  • Static string with Rust core version (e.g., “0.1.0-beta”)
  • Never needs to be freed (static lifetime)

Result Codes

#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChameleonResult {
    Ok = 0,
    ParseError = 1,
    ValidationError = 2,
    InternalError = 3,
}
Usage in Go:
const (
    ResultOk              = C.CHAMELEON_OK
    ResultParseError      = C.CHAMELEON_PARSE_ERROR
    ResultValidationError = C.CHAMELEON_VALIDATION_ERROR
    ResultInternalError   = C.CHAMELEON_INTERNAL_ERROR
)

Go Bindings

Location: chameleon/internal/ffi/bindings.go

CGO Setup

package ffi

/*
#cgo linux LDFLAGS: -L/usr/local/lib -Wl,-rpath,/usr/local/lib -lchameleon
#cgo darwin LDFLAGS: -L/usr/local/lib -L/opt/homebrew/lib -Wl,-rpath,/usr/local/lib -Wl,-rpath,/opt/homebrew/lib -lchameleon
#include <stdlib.h>

typedef enum {
    CHAMELEON_OK = 0,
    CHAMELEON_PARSE_ERROR = 1,
    CHAMELEON_VALIDATION_ERROR = 2,
    CHAMELEON_INTERNAL_ERROR = 3,
} ChameleonResult;

char* chameleon_parse_schema(const char* input, char** error_out);
ChameleonResult chameleon_validate_schema(const char* schema_json, char** error_out);
void chameleon_free_string(char* s);
const char* chameleon_version(void);
extern int chameleon_generate_sql(const char* query_json, const char* schema_json, char** error_out);
extern int chameleon_generate_migration(const char* schema_json, char** error_out);
*/
import "C"
LDFLAGS explanation:
  • -L/usr/local/lib - Library search path
  • -Wl,-rpath,/usr/local/lib - Runtime library path (embeds path in binary)
  • -lchameleon - Link against libchameleon.so (Linux) or libchameleon.dylib (macOS)

ParseSchema

func ParseSchema(input string) (string, error) {
    cInput := C.CString(input)
    defer C.free(unsafe.Pointer(cInput))
    
    var cError *C.char
    defer func() {
        if cError != nil {
            C.chameleon_free_string(cError)
        }
    }()
    
    cResult := C.chameleon_parse_schema(cInput, &cError)
    
    if cResult == nil {
        if cError != nil {
            return "", errors.New(C.GoString(cError))
        }
        return "", errors.New("unknown parse error")
    }
    
    defer C.chameleon_free_string(cResult)
    return C.GoString(cResult), nil
}
Memory management:
  1. C.CString(input) - Allocate C string (must free with C.free)
  2. C.chameleon_parse_schema() - Call Rust (returns Rust-allocated string)
  3. C.GoString(cResult) - Copy to Go string
  4. C.chameleon_free_string(cResult) - Free Rust allocation

ValidateSchemaRaw

func ValidateSchemaRaw(schemaInput string) (string, error) {
    cInput := C.CString(schemaInput)
    defer C.free(unsafe.Pointer(cInput))
    
    var cError *C.char
    defer func() {
        if cError != nil {
            C.chameleon_free_string(cError)
        }
    }()
    
    result := C.chameleon_validate_schema(cInput, &cError)
    
    if cError != nil {
        errMsg := C.GoString(cError)
        if result == ResultOk {
            return errMsg, nil  // Success JSON
        }
        return errMsg, errors.New("validation failed")
    }
    
    return "", errors.New("unknown error")
}

Version

func Version() string {
    cVersion := C.chameleon_version()
    return C.GoString(cVersion)
}
Note: No need to free - static string in Rust

Building libchameleon.so

Prerequisites

# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Install build dependencies
sudo apt-get install build-essential  # Linux
brew install llvm                      # macOS

Build Steps

cd chameleon-core

# Build shared library
cargo build --release

# Output: target/release/libchameleon.so (Linux)
#         target/release/libchameleon.dylib (macOS)
#         target/release/chameleon.dll (Windows)

Install System-Wide

Linux:
sudo cp target/release/libchameleon.so /usr/local/lib/
sudo ldconfig
macOS:
sudo cp target/release/libchameleon.dylib /usr/local/lib/
sudo update_dyld_shared_cache  # Optional, speeds up loading

Verify Installation

# Check library dependencies
ldd /usr/local/lib/libchameleon.so     # Linux
otool -L /usr/local/lib/libchameleon.dylib  # macOS

# Test from Go
cd ../chameleon
go test ./internal/ffi/...

Cargo Configuration

Location: chameleon-core/Cargo.toml
[package]
name = "chameleon"
version = "0.1.0-beta"
edition = "2021"

[lib]
name = "chameleon"
crate-type = ["rlib", "staticlib", "cdylib"]

[dependencies]
lalrpop-util = { version = "0.20", features = ["lexer"] }
regex = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
lazy_static = "1.4"

[build-dependencies]
lalrpop = "0.20"
cbindgen = "0.27"
Crate types:
  • rlib - Rust static library (for Rust consumers)
  • staticlib - C-compatible static library (.a)
  • cdylib - C-compatible dynamic library (.so, .dylib, .dll)

C Header Generation

Location: chameleon-core/build.rs:22
cbindgen::Builder::new()
    .with_crate(crate_dir)
    .with_language(cbindgen::Language::C)
    .with_include_guard("CHAMELEON_H")
    .generate()
    .expect("Unable to generate bindings")
    .write_to_file("include/chameleon.h");
Generated header (include/chameleon.h):
#ifndef CHAMELEON_H
#define CHAMELEON_H

#include <stdint.h>

typedef enum {
    ChameleonResult_Ok = 0,
    ChameleonResult_ParseError = 1,
    ChameleonResult_ValidationError = 2,
    ChameleonResult_InternalError = 3,
} ChameleonResult;

char *chameleon_parse_schema(const char *input, char **error_out);

ChameleonResult chameleon_validate_schema(const char *input, char **error_out);

void chameleon_free_string(char *s);

const char *chameleon_version(void);

ChameleonResult chameleon_generate_sql(const char *query_json,
                                        const char *schema_json,
                                        char **error_out);

ChameleonResult chameleon_generate_migration(const char *schema_json,
                                              char **error_out);

#endif /* CHAMELEON_H */

Linking from Go

Development Build

# Build Rust library
cd chameleon-core
cargo build --release

# Set library path for Go
export LD_LIBRARY_PATH=$PWD/target/release:$LD_LIBRARY_PATH  # Linux
export DYLD_LIBRARY_PATH=$PWD/target/release:$DYLD_LIBRARY_PATH  # macOS

# Build Go application
cd ../chameleon
go build ./cmd/chameleon

Production Build

# Install library system-wide
sudo cp chameleon-core/target/release/libchameleon.so /usr/local/lib/
sudo ldconfig

# Go will find library via rpath (set in cgo LDFLAGS)
go build ./cmd/chameleon
./chameleon --version

Docker Build

FROM rust:1.75 AS rust-builder
WORKDIR /build
COPY chameleon-core .
RUN cargo build --release

FROM golang:1.21 AS go-builder
WORKDIR /build
COPY --from=rust-builder /build/target/release/libchameleon.so /usr/local/lib/
RUN ldconfig
COPY chameleon .
RUN go build -o chameleon ./cmd/chameleon

FROM ubuntu:22.04
COPY --from=rust-builder /build/target/release/libchameleon.so /usr/local/lib/
COPY --from=go-builder /build/chameleon /usr/local/bin/
RUN ldconfig
ENTRYPOINT ["chameleon"]

Memory Management

Ownership Rules

AllocatorDeallocatorExample
Go (C.CString)Go (C.free)Input strings to Rust
Rust (CString::into_raw)Rust (chameleon_free_string)Return values from Rust
Rust (static)Neverchameleon_version()

Common Pitfalls

❌ Double-free:
result := C.chameleon_parse_schema(input, &err)
C.chameleon_free_string(result)
C.chameleon_free_string(result)  // CRASH!
❌ Memory leak:
result := C.chameleon_parse_schema(input, &err)
return C.GoString(result)  // Leak! Never freed
✅ Correct:
result := C.chameleon_parse_schema(input, &err)
defer C.chameleon_free_string(result)
return C.GoString(result)

Performance Characteristics

OperationOverheadNotes
FFI call~100nsFunction call + argument marshaling
String copy (Go → C)~50ns/KBC.CString() allocation
String copy (C → Go)~50ns/KBC.GoString() allocation
JSON serialization~1μs/entityserde_json::to_string()
JSON deserialization~2μs/entityjson.Unmarshal() in Go
Total parse overhead:
  • Small schema (5 entities): ~500μs
  • Medium schema (20 entities): ~2ms
  • Large schema (100 entities): ~10ms
Comparison to pure Go parser:
  • FFI overhead: +30% (but Rust parser is 3x faster)
  • Net result: 2x faster than pure Go

Testing

Location: chameleon-core/src/ffi/mod.rs:471

Rust Tests

#[test]
fn test_parse_schema_success() {
    let input = CString::new("entity User { id: uuid primary, }").unwrap();
    let mut error: *mut c_char = ptr::null_mut();
    
    unsafe {
        let result = chameleon_parse_schema(input.as_ptr(), &mut error);
        assert!(!result.is_null());
        assert!(error.is_null());
        
        let json_str = CStr::from_ptr(result).to_str().unwrap();
        assert!(json_str.contains("User"));
        
        chameleon_free_string(result);
    }
}

Go Tests

Location: chameleon/internal/ffi/bindings_test.go (hypothetical)
func TestParseSchema(t *testing.T) {
    input := `entity User { id: uuid primary, }`
    result, err := ParseSchema(input)
    
    require.NoError(t, err)
    assert.Contains(t, result, "User")
    
    var schema map[string]interface{}
    err = json.Unmarshal([]byte(result), &schema)
    require.NoError(t, err)
}

func TestVersion(t *testing.T) {
    version := Version()
    assert.NotEmpty(t, version)
    assert.Regexp(t, `^\d+\.\d+\.\d+`, version)
}

Troubleshooting

Library Not Found

Error:
error while loading shared libraries: libchameleon.so: cannot open shared object file
Solution:
# Add to LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH

# Or install system-wide
sudo cp libchameleon.so /usr/local/lib/
sudo ldconfig

Symbol Not Found

Error:
undefined reference to `chameleon_parse_schema'
Causes:
  • Rust library not built with cdylib crate type
  • Function not marked #[no_mangle]
  • Header out of sync with implementation
Solution:
# Rebuild with correct crate type
cargo clean
cargo build --release

# Verify symbols
nm -D target/release/libchameleon.so | grep chameleon

Version Mismatch

Error:
FFI version mismatch: Go expects 0.1.0, Rust is 0.2.0
Solution:
# Check versions
go run ./cmd/chameleon version
cargo metadata --format-version 1 | jq -r '.packages[] | select(.name == "chameleon") | .version'

# Rebuild both
cd chameleon-core && cargo build --release
cd ../chameleon && go build ./cmd/chameleon

See Also