OpenSysML can be reached from a program in seven ways: the Go API, which runs in the calling
process, and six clients of the sysml-grpc service. This page describes how to choose between
them, what each covers and what each intentionally leaves out. Each client has an API reference of
its own, and the client guides walk through a task with each one.
| Surface | Reaches the engine by | Published | Full reference |
|---|---|---|---|
Go, client/opensysml |
in process; or Connect, to a service someone else runs | with the core (v* tags) |
Go packages |
Python, opensysml |
gRPC, to a private child service or a named service | PyPI, on the core v* tags, at the core's version |
Python API |
Node/TypeScript, @openmbee/opensysml |
Connect, to a private child service, a named service, or one a browser page addresses | npm, on core v* tags, with per-platform binary packages |
Node API |
Java, org.openmbee:opensysml |
Connect, over the JDK's own HTTP client | not on Maven Central; build from a checkout | Java API |
Rust, opensysml |
Connect, blocking, no async runtime | crates.io, on core v* tags, at the core version |
Rust API |
Julia, OpenSysML |
Connect-JSON, over HTTP.jl |
not in General; develop from a checkout | Julia API |
MATLAB, +opensysml |
Connect-JSON, over matlab.net.http or, under GNU Octave, a curl subprocess |
source files; add to the MATLAB path | MATLAB API |
The protocols and what the service serves on a single port are described in service transports; the release process for each client is described in releasing.
- In a Go program:
client/opensysml. It links the parser, the semantic engine and the runtime directly, so there is no port, no child process and no serialization round trip. A Go program that starts a service to talk to itself is paying for a child process whose only job is to run code the program already links. - In a notebook: Python.
opensysmladds generated typed classes, Jupyter display hooks and DataFrame integration to the full RPC surface. - In a browser or a Node service:
@openmbee/opensysml. No native addon, and the browser entry point needs onlyfetchagainst a service that allows the page's origin. - In a JVM host application the caller does not control (an Eclipse-based tool, a Cameo plugin,
a web application): Java. Its transport is
java.net.http.HttpClient, so no gRPC, Netty ortcnativedependency reaches the host application. - In a Rust program:
opensysml. Blocking, with no asynchronous runtime in its default dependency tree, and safe to call from inside one. - In a Julia session or script:
OpenSysML. A thin JSON-over-HTTP client —HTTP.jlandJSON.jlare its only dependencies — that parses, evaluates, instantiates, executes and queries, withcallas the escape hatch for anything not wrapped. - In MATLAB or GNU Octave:
+opensysml. The same thin client for the environments a modeler already runs; Octave 7+ is the tested path.
The Go, Java, Julia and MATLAB clients each reach every RPC the service has — the Julia
and MATLAB ones through call/callRaw under the wrapped functions, so nothing on the wire is
out of reach — and so do the Python, Node and Rust clients.
The Node client covers everything the Python one does, the whole service surface included.
The Java client covers the whole service surface, as typed immutable results:
- the v1 calls — parsing, diagnostics, symbol lookup, evaluation and instantiation — plus
parseSourcesfor a model of several documents,convert/convertFile/Model.convertfor conversion between notations andmigrate/migrateFilefor migrating a SysML v1 model with its element-by-element report; - verification (
VerifyConstraint,VerifyRequirement,VerifySatisfaction,ValidateInstance), keeping a false verdict as an answer rather than a failure; EvaluateCalcandRunAnalysis, withListEnginesand theengineselection they take, and the partial result a failed analysis leaves behind;- behaviour execution (
ExecuteAction,ExecuteState), single runs and exploration of every schedule, andRunSweepparameter sweeps; - the edit API (
applyEdits, with theEditkinds sealed over set-value, rename, add-member, delete and move); Queryand OSLC query, and the native document calls (runDocumentQuery,renderDocument).
It leaves out only the generated model-ergonomics types; the Java API says why.
The Rust client covers the same service surface as typed results — parse_sources, convert,
query/query_oslc, the document calls, behaviour execution and exploration, verification,
calc, run_analysis, run_sweep, list_engines and an Editor over ApplyEdits — as the
Rust API describes.
Those RPCs exist and are served. ApplyEdits also edits a model of several documents, parsed
together by ParseSources, as one atomic batch — every document the edits reach is answered in
ApplyEditsResponse.documents under the name the parse gave it, and the sole-document content
stays filled for a model of one document (the wire contract).
A request must set accept_documents for that; one that does not is refused on a model of several
documents as before, so a client of the previous schema is answered as it always was. The service
advertises the edit_documents capability for it; one without the capability answers content
alone and refuses a model of several documents, so a client reads documents only from a service
that advertises it. The Go, Python, Java, Node and Rust clients set it and expose the documents.
Each client's conformance report names, per scenario, why a skipped scenario is skipped, so a shrinking surface cannot pass quietly.
Only the generated model-ergonomics types differ: the Node client generates typed modules with
opensysml-generate, and the Go, Java and Rust APIs read models through Symbol, Instance and
Value instead.
Every client that can start a service starts a private child of the calling process
(sysml-grpc -port 0 -health-port 0 -report-address -exit-with-parent) and reads the address the
kernel assigned from the child's first line of stdout. No port is chosen, probed or retried, so two
processes starting at once cannot collide, and a service left listening by someone else is never
adopted. The child is shared within a scope, and so is its parse cache: per interpreter in
Python, per thread in Node, per classloader in Java (isolatedService(true) opts out), per
process in Rust. client/opensysml starts nothing, because in process there is nothing to start.
Connecting to a service the client did not start is always explicit, through an address argument or
$OPENSYSML_SERVICE, and closing such a connection disconnects and does nothing further.
No orphans, and the mechanism is not an exit hook. Each client holds the write end of the
child's stdin pipe and never writes to it; the child exits at end of file. The kernel closes that
pipe when the holder dies, however it dies, which covers cases a shutdown hook does not:
SIGKILL, Runtime.halt, process.abort(), a crash during shutdown. Every client pins this
behavior with a test that kills its own parent process and asserts the service is gone.
Every client sends protobuf bodies by default and offers JSON for curl-based debugging. This
reflects a measurement rather than a preference: a 468 KB response costs about 6.5 ms with
a protobuf body against about 42 ms with JSON, and the difference is protojson and json_format
CPU time rather than bytes on the wire. See service transports.
An analysis environment with no client above — R, C, a shell script — can still
reach the service, because Connect with a JSON body is an ordinary HTTP POST its own HTTP
library can make. What such a hand-written client has to decode is written down once, field by
field and with every example captured from a running service, on
the wire contract: the ParseSources session and how long a modelHash
lives, every arm of Value and how to tell them apart, diagnostics against Connect errors, the
behavior and query answer shapes, and a short illustrative decoder in each of the four
languages. The Julia and MATLAB illustrations are superseded by the shipped client/julia and
client/matlab packages; the R and C snippets remain illustrations, and a client that claims to
be one runs the conformance scenarios below through its own API.
Python, Node, Java and Julia download binaries pinned by per-release-asset SHA-256 digests; Python,
Node and Java verify the release's Sigstore-signed manifest, but Julia does not. A crate published from a Rust
release tag embeds that release's service digests and downloads its built-against release by default;
Rust does not verify the manifest's Sigstore signature itself. Clients also look for explicitly
supplied or already-installed binaries, using the same lookup order:
$OPENSYSML_GRPC_BINARY ($OPENSYSML_BINARY in the Node and Python clients) first, then
~/.opensysml/bin/sysml-grpc (where a verified download puts it), then PATH. The Node client
also checks its per-platform npm package, whose tarball npm verifies, with no postinstall script;
that package is preferred over a download, which happens only when no package matches the platform.
The Java client additionally verifies a digest the caller pins with expectedBinarySha256.
If no binary can be found or downloaded, the result is an error naming every way to supply one.
The scenarios in conformance/
are the service contract, and each client runs them through its own public API rather than
through generated stubs, so conformance with sysml-grpc is measured per language, over the same
scenarios and comparing the same results:
make conformance # the reference runner: gRPC, Connect, Connect-JSON
make conformance-pkg # the public Go API, in process and remote
make conformance-rust
make conformance-julia
make conformance-matlab
npm --prefix client/node run conformance -- --allow-skipsThe Java runner is launched from its own classpath rather than by a Maven goal; the two exact commands are given in client/java/README.md.
The reference runner also takes -junit <file>, writing the same run as JUnit XML (one suite per
configuration and protocol, one case per scenario). That is what make conformance stores beside
the JSON report and what CI renders as its test report. The JSON report stays the source of truth.
Each runner writes the report format produced by tools/cmd/conformance, and each is checked against
deliberate corruption (a mutated response must fail a scenario), so a runner that asserts nothing
cannot pass. Current per-client scenario counts are given in each client's README; they change as
v1 gaps close, which is why they are maintained beside the code rather than here.