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.
Get the library
Section titled “Get the library”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:
tar -xzf tinyconductor-0.1.0-linux-x86_64.tar.gz| File | Platform |
|---|---|
lib/libbpm_embedded_ffi.so | Linux |
lib/libbpm_embedded_ffi.dylib | macOS |
lib/bpm_embedded_ffi.dll | Windows |
include/bpm_embedded.h | the 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.
The ABI
Section titled “The ABI”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_destroydrops 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 whenstatusis notBPM_EMBEDDED_OK. Copy what you need, then release it withbpm_embedded_buffer_free, exactly once. - Status codes:
OK0,INVALID_ARGUMENT1,NOT_FOUND2,MODE_NOT_SUPPORTED3,ENGINE_REJECTED4,PANIC5,INVALID_UTF86,INTERNAL7. A panic inside the engine is contained and reported asPANIC; 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"}}, whereproblemis the server’s problem document when the engine refused the request.
Operations
Section titled “Operations”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.
operation | Fields | Answer |
|---|---|---|
deploy | resources: [{"name", "content"}] (content is the file’s text) | deploymentKey, deployments (resourceType, resourceId, version, digest, deduplicated) |
createInstance | processId, variables | processInstanceKey, … |
cancelInstance | processInstanceKey | {} |
setVariables | processInstanceKey, variables, optional scopeKey, local | {} |
publishMessage | name, correlationKey, optional timeToLive, variables, messageId | messageKey |
broadcastSignal | name, variables | broadcastKey |
activateJobs | type, optional timeout (30000), maxJobsToActivate (1) | jobs: jobKey, leaseEpoch, variables, deadline, … (answers at once, also when there is no job) |
completeJob | jobKey, leaseEpoch, variables | {} |
failJob | jobKey, leaseEpoch, retries, optional retryBackOff, errorMessage | {} |
throwJobError | jobKey, leaseEpoch, errorCode, optional errorMessage | {} |
updateJobTimeout | jobKey, timeout | {} |
resolveIncident | incidentKey, optional retries | {} |
assignUserTask, claimUserTask | userTaskKey, processInstanceKey, assignee | {} |
unclaimUserTask | userTaskKey, processInstanceKey | {} |
updateUserTask | userTaskKey, processInstanceKey, optional candidateGroups, candidateUsers, dueDate, followUpDate | {} |
completeUserTask | userTaskKey, processInstanceKey, variables | {} |
records | — | {"records": [...]} |
clockReport, freezeClock | — | the clock |
advanceClockBy | milliseconds | the clock |
advanceClockTo | instant (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 test in C
Section titled “The test in C”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/cd tinyconductor-0.1.0-examples/testing/cmake testThe 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.
From Python with ctypes
Section titled “From Python with ctypes”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 = CreateResultlib.bpm_embedded_dispatch.restype = Bufferlib.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().handleprint(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.