Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

C ABI: Embedding & Writing New Bindings

For language runtimes without direct Rust interoperability libraries (like pyo3 or napi-rs), datalogic-rs exposes a stable C ABI in bindings/c. It is how the Go, JVM (Java/Kotlin), .NET (C#), and PHP bindings talk to the core, and it is the starting point if you want to embed the engine in a language we don’t ship yet.

+-------------------+
| datalogic-rs Core |
+---------+---------+
          | (Rust path-dependency)
+---------v---------+
|    bindings/c     | (C ABI, generates datalogic.h / libdatalogic_c)
+----+----+----+----+
     |    |    |    |
     |    |    |    +---> PHP FFI (goplasmatic/datalogic)
     |    |    +--------> .NET P/Invoke (Goplasmatic.Datalogic)
     |    +-------------> JVM FFM (io.github.goplasmatic:datalogic)
     +------------------> Go cgo (github.com/GoPlasmatic/datalogic-rs/bindings/go/v5)

The full function-by-function surface, build instructions, and cbindgen notes live in the C ABI README.

Binary distribution

Because these bindings rely on compiled shared/static libraries, the release pipeline compiles the bindings/c code for all supported operating systems and architectures. The binaries are then bundled into the standard package layout for each ecosystem.

EcosystemPackagingBinaries LayoutLoading Mechanism
GoGo ModuleStatic libraries in lib/<os>_<arch>/cgo static linking at compile time
JVMMaven JARShared libraries at the classpath root under <os-arch>/FFM (java.lang.foreign) at runtime
.NETNuGetShared libraries under runtimes/<rid>/native/P/Invoke LibraryImport at runtime
PHPComposerShared libraries under lib/<os>-<arch>/PHP FFI::cdef at runtime

The JSON-in/JSON-out rule

To keep the C ABI surface simple and performant, inputs and outputs crossing the boundary are UTF-8 JSON strings passed as (pointer, length) pairs (ABI v2 carries an explicit byte length, so there are no NUL terminators and embedded NULs or non-ASCII bytes are safe). No complex struct marshaling is performed at the boundary. Instead, inputs are serialized to JSON in the host language, passed to Rust, evaluated, and the result is returned as JSON bytes to be parsed back by the host.

Memory management & safety

Because the Go, JVM, .NET, and PHP bindings interface with the Rust core over a C FFI boundary, memory management rules differ significantly from native Go/Java/C#/PHP code.

⚠️ The danger: native memory leaks

When you instantiate an Engine or compile a Rule in a managed language, the actual structures (optimized bytecode ASTs, configuration options, operator collections) are allocated on the native Rust heap, and only a raw 64-bit memory pointer is returned to your host language.

Managed garbage collectors (like the JVM, .NET CLR, Go’s GC, or PHP’s Zend GC) only track the size of the wrapper object itself (which is usually a few bytes representing the pointer address). The GC has no awareness of the potentially megabytes of memory allocated on the native heap behind that pointer.

If you let these wrapper objects go out of scope without calling their destructors, the native memory will leak permanently until the host process terminates.

🛡️ Best practices per language

Follow these patterns to ensure leak-free evaluation:

🟢 Go: explicit cleanup with defer

Go does not support object finalizers or automatic destructors for local variables. You must call .Close() explicitly.

engine := datalogic.NewEngine()
defer engine.Close() // ALWAYS defer Close

rule, err := engine.Compile(ruleJSON)
if err != nil {
    return err
}
defer rule.Close() // ALWAYS defer Close

session := engine.Session()
defer session.Close() // ALWAYS defer Close

☕ JVM: try-with-resources

Java and Kotlin provide the try-with-resources statement. All datalogic classes implement AutoCloseable, making this the cleanest and safest pattern:

// Automatic closure of Engine and Rule
try (Engine engine = new Engine();
     Rule rule = engine.compile(ruleStr)) {

    // Automatic closure of Session
    try (Session session = engine.openSession()) {
        String result = session.evaluate(rule, data);
    }
} // Engine, Rule, and Session are guaranteed to be closed here

🔷 .NET: using statements

In C#, use the using keyword. If you forget, the C# wrapper provides a finalizer fallback, but explicit disposal is highly recommended:

using var engine = new Engine();
using var rule = engine.Compile(ruleJSON);

using (var session = engine.OpenSession())
{
    var result = session.Evaluate(rule, data);
} // Session is disposed here
// Engine and Rule are disposed when the current method scope ends

🐘 PHP: scope-destructors & close()

PHP releases FFI objects when they fall out of scope. However, for CLI daemons, Swoole services, or long-running PHP-FPM requests, always close handles manually:

$engine = new Engine();
$rule = $engine->compile($ruleJSON);

$session = $engine->openSession();
$result = $session->evaluate($rule, $data);

// Explicit cleanup prevents memory creep in long-running processes
$session->close();
$rule->close();
$engine->close();

🧵 Thread safety & concurrency

When sharing compiled logic across multiple threads, remember the following thread-safety boundaries:

Class / TypeThread-Safe?Usage Pattern
EngineYesConstruct once globally; share across all threads/goroutines.
RuleYesCompile once; share and call Evaluate() concurrently.
SessionNoNever share sessions. Keep one Session instance per thread.
TracedSessionYesOpen once; evaluate concurrently.

Why Session is not thread-safe

Session contains a fast, zero-copy bumpalo arena allocator. It works by moving a cursor forward on a pre-allocated memory page. If two threads evaluate logic concurrently using the same session, they will overwrite each other’s memory, leading to crashes or data corruption.