Skip to content

Testing with C and other FFI languages

The Embedded engine is also a native library with a small C ABI, bpm-embedded-ffi. Anything that can call a C function — C, C++, Swift, Zig, Python’s ctypes, Java’s FFM API, Node-API addons and so on — can host an engine inside its own process. The .NET SDK is built on exactly this ABI.

The library is the Embedded server itself, without a port: each operation answers exactly as the matching request to a server started with --mode embedded (see In-process hosting for which request each operation is).

The runnable project is testing/c in the examples download.

The library ships in every release archive, next to the tinyconductor program. Download the archive for your platform (the list is in Embedded mode) and unpack it:

Terminal window
tar -xzf tinyconductor-0.1.0-linux-x86_64.tar.gz
FilePlatform
lib/libbpm_embedded_ffi.soLinux
lib/libbpm_embedded_ffi.dylibmacOS
lib/bpm_embedded_ffi.dllWindows
include/bpm_embedded.hthe header, for every platform

Compile against the header and link the library, for example cc -I tinyconductor-0.1.0-linux-x86_64/include my_test.c -L tinyconductor-0.1.0-linux-x86_64/lib -lbpm_embedded_ffi. At run time the library must be where your loader finds it: next to your test program, or on LD_LIBRARY_PATH (Linux) or DYLD_LIBRARY_PATH (macOS). There is no package-manager release of the library yet; the release archive is how it is distributed.

BpmEmbeddedCreateResult bpm_embedded_create(void); /* {status, handle} */
int32_t bpm_embedded_destroy(uint64_t handle);
BpmEmbeddedBuffer bpm_embedded_dispatch(uint64_t handle, const uint8_t *request,
size_t request_len); /* one JSON operation */
BpmEmbeddedBuffer bpm_embedded_advance_by(uint64_t handle, int64_t milliseconds);
BpmEmbeddedBuffer bpm_embedded_reset(uint64_t handle);
BpmEmbeddedBuffer bpm_embedded_clock_report(uint64_t handle);
void bpm_embedded_buffer_free(BpmEmbeddedBuffer buffer);
  • A handle is one isolated engine. bpm_embedded_destroy drops it and everything it held.
  • Every call that answers returns a BpmEmbeddedBuffer {status, data, len} holding UTF-8 JSON — the result, or an error document when status is not BPM_EMBEDDED_OK. Copy what you need, then release it with bpm_embedded_buffer_free, exactly once.
  • Status codes: OK 0, INVALID_ARGUMENT 1, NOT_FOUND 2, MODE_NOT_SUPPORTED 3, ENGINE_REJECTED 4, PANIC 5, INVALID_UTF8 6, INTERNAL 7. A panic inside the engine is contained and reported as PANIC; it never unwinds into your process.
  • Calls on one handle are processed one at a time; separate handles run independently. Clock advances return only after all work due at the new instant has run.
  • An error document is {"error": {"code", "message", "problem"}}, where problem is the server’s problem document when the engine refused the request.

bpm_embedded_dispatch takes one JSON object whose operation field names the operation. Keys go in as JSON numbers or strings and come back as JSON strings. Answers are the server’s answers, so they can carry more fields than the table lists.

operationFieldsAnswer
deployresources: [{"name", "content"}] (content is the file’s text)deploymentKey, deployments (resourceType, resourceId, version, digest, deduplicated)
createInstanceprocessId, variablesprocessInstanceKey, …
cancelInstanceprocessInstanceKey{}
setVariablesprocessInstanceKey, variables, optional scopeKey, local{}
publishMessagename, correlationKey, optional timeToLive, variables, messageIdmessageKey
broadcastSignalname, variablesbroadcastKey
activateJobstype, optional timeout (30000), maxJobsToActivate (1)jobs: jobKey, leaseEpoch, variables, deadline, … (answers at once, also when there is no job)
completeJobjobKey, leaseEpoch, variables{}
failJobjobKey, leaseEpoch, retries, optional retryBackOff, errorMessage{}
throwJobErrorjobKey, leaseEpoch, errorCode, optional errorMessage{}
updateJobTimeoutjobKey, timeout{}
resolveIncidentincidentKey, optional retries{}
assignUserTask, claimUserTaskuserTaskKey, processInstanceKey, assignee{}
unclaimUserTaskuserTaskKey, processInstanceKey{}
updateUserTaskuserTaskKey, processInstanceKey, optional candidateGroups, candidateUsers, dueDate, followUpDate{}
completeUserTaskuserTaskKey, processInstanceKey, variables{}
records—{"records": [...]}
clockReport, freezeClock—the clock
advanceClockBymillisecondsthe clock
advanceClockToinstant (RFC 3339)the clock
reset—the clock at the epoch

There is no user-task search in this interface: take a task’s userTaskKey from its USER_TASK / CREATED record.

A job’s timeout runs on the engine’s clock. If your test moves the clock past it before completing or failing the job, the job is offered again with a new leaseEpoch, and the old one can no longer complete it — as on every server.

The heart of the example (JSON handling kept to strstr; use a JSON library in real code):

BpmEmbeddedCreateResult created = bpm_embedded_create();
engine = created.handle;
deploy("order.form"); /* {"operation":"deploy","resources":[{"name":…,"content":…}]} */
deploy("order-decisions.dmn");
deploy("order.bpmn");
char *answer = dispatch("{\"operation\":\"createInstance\",\"processId\":\"order-process\","
"\"variables\":{\"orderId\":4711,\"amount\":99.9}}");
long long instance = number_field(answer, "processInstanceKey");
complete_job("charge-card", "{\"charged\":true}"); /* activateJobs, then completeJob */
BpmEmbeddedBuffer clock = bpm_embedded_advance_by(engine, 86400000); /* timer fires here */
bpm_embedded_buffer_free(clock);
complete_job("send-invoice", "{}");
/* completeUserTask with the key from the USER_TASK CREATED record, then
check the PROCESS_INSTANCE ELEMENT_COMPLETED records. */
bpm_embedded_destroy(engine);

Run it with the release archive for your platform unpacked next to the examples download, in the same folder:

downloads/
tinyconductor-0.1.0-examples/
tinyconductor-0.1.0-linux-x86_64/
Terminal window
cd tinyconductor-0.1.0-examples/testing/c
make test

The Makefile takes the library from the archive’s lib folder and the header from its include folder, and picks the archive for your platform (linux-x86_64, linux-arm64 or macos-arm64). If you unpacked the archive somewhere else, name it: make test TINYCONDUCTOR_HOME=/path/to/tinyconductor-0.1.0-linux-x86_64.

The same ABI from Python’s standard library:

import ctypes, json
lib = ctypes.CDLL("tinyconductor-0.1.0-macos-arm64/lib/libbpm_embedded_ffi.dylib")
class CreateResult(ctypes.Structure):
_fields_ = [("status", ctypes.c_int32), ("handle", ctypes.c_uint64)]
class Buffer(ctypes.Structure):
_fields_ = [("status", ctypes.c_int32), ("data", ctypes.POINTER(ctypes.c_uint8)),
("len", ctypes.c_size_t)]
lib.bpm_embedded_create.restype = CreateResult
lib.bpm_embedded_dispatch.restype = Buffer
lib.bpm_embedded_dispatch.argtypes = [ctypes.c_uint64, ctypes.c_char_p, ctypes.c_size_t]
lib.bpm_embedded_buffer_free.argtypes = [Buffer]
lib.bpm_embedded_destroy.argtypes = [ctypes.c_uint64]
def dispatch(handle, request):
raw = json.dumps(request).encode()
buffer = lib.bpm_embedded_dispatch(handle, raw, len(raw))
try:
answer = json.loads(ctypes.string_at(buffer.data, buffer.len))
finally:
lib.bpm_embedded_buffer_free(buffer)
if buffer.status != 0:
raise RuntimeError(answer)
return answer
engine = lib.bpm_embedded_create().handle
print(dispatch(engine, {"operation": "clockReport"}))
lib.bpm_embedded_destroy(engine)

This snippet was run as shown; the full order scenario from Python is in the Python guide, which uses the server form.