Language bindings
SAPIENT bindings for Rust, Python, C#, Go and Java
Generated BSI Flex 335 message types and SAPIENT's TCP framing, packaged for each language's usual package manager. Install one and you can send and receive SAPIENT messages without first writing protobuf build steps and framing code.
Packages
All five are built from the same copy of the SAPIENT schema in the sapient-bindings repository, and include both BSI Flex 335 v1.0 and v2.0 message definitions.
| Language | Package | Install |
|---|---|---|
| Rust | sapient-rs on crates.io | cargo add sapient-rs |
| Python | sapient-py on PyPI | pip install sapient-py |
| C# | Sapient.Bindings on NuGet | dotnet add package Sapient.Bindings |
| Go | go/sapient on pkg.go.dev | go get github.com/tom-mann-ironclad/sapient-bindings/go/sapient/... |
| Java | sapient-bindings on Maven Central | io.github.tom-mann-ironclad:sapient-bindings:0.1.0(Maven and Gradle snippets) |
SAPIENT framing
Protobuf doesn't mark where one message ends and the next begins, so SAPIENT puts each SapientMessage on a TCP stream behind a 4-byte unsigned little-endian length:
| length (u32, little-endian) | SapientMessage protobuf bytes (length bytes) |
Getting this wrong (big-endian, or a missing prefix) is a common first-day integration bug. Each package provides encode/decode helpers for single frames and read/write helpers that work on a stream, so you don't have to write it yourself.
Each example below is the start of a Fusion Node: it accepts one connection and prints each message it receives. Point the test harness at it with sapient-harness session --role edge --target 127.0.0.1:5000 and you'll see its Registration arrive. A real Fusion Node would answer with a RegistrationAck; until it does, a conformant Edge Node sends nothing else.
Rust
sapient-rs builds on prost. The v2_0 schema is enabled by default; enable the v1_0 feature for the older schema. Its async framing helpers work with any Tokio stream.
// cargo add sapient-rs tokio --features tokio/full
use std::pin::Pin;
use sapient_rs::bsi_flex_335_v2_0::SapientMessage;
use tokio::net::TcpListener;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:5000").await?;
let (mut stream, peer) = listener.accept().await?;
loop {
let msg: SapientMessage = sapient_rs::utils::read(Pin::new(&mut stream)).await?;
println!("{peer}: {:?}", msg.content);
}
}
Send with sapient_rs::utils::send(message, Pin::new(&mut stream)).await.
Python
Generated modules are under sapient_msg; the framing helpers are in sapient_py and work on any binary file-like object, including a socket's makefile().
import socket
from sapient_msg.bsi_flex_335_v2_0.sapient_message_pb2 import SapientMessage
from sapient_py import read_frame
with socket.create_server(("127.0.0.1", 5000)) as server:
conn, peer = server.accept()
with conn, conn.makefile("rb") as stream:
while True:
msg = read_frame(stream, SapientMessage)
print(peer, msg.WhichOneof("content"))
Send with write_frame(stream, message), or encode_frame(message) for the raw bytes.
C#
Sapient.Bindings targets .NET Standard 2.0. Message types are in Sapient.Bindings.BsiFlex335.V2_0.
using System.Net;
using System.Net.Sockets;
using Sapient.Bindings;
using Sapient.Bindings.BsiFlex335.V2_0;
var listener = new TcpListener(IPAddress.Loopback, 5000);
listener.Start();
using var client = listener.AcceptTcpClient();
using var stream = client.GetStream();
while (true)
{
var msg = SapientFraming.ReadFrame(stream, SapientMessage.Parser);
Console.WriteLine($"{client.Client.RemoteEndPoint}: {msg.ContentCase}");
}
Send with SapientFraming.WriteFrame(stream, message).
Go
Generated types build on google.golang.org/protobuf; framing.ReadFrame and framing.WriteFrame take any io.Reader or io.Writer.
package main
import (
"fmt"
"log"
"net"
"github.com/tom-mann-ironclad/sapient-bindings/go/sapient/framing"
v2 "github.com/tom-mann-ironclad/sapient-bindings/go/sapient/generated/bsi_flex_335_v2_0"
)
func main() {
ln, err := net.Listen("tcp", "127.0.0.1:5000")
if err != nil {
log.Fatal(err)
}
conn, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
for {
var msg v2.SapientMessage
if err := framing.ReadFrame(conn, &msg); err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %T\n", conn.RemoteAddr(), msg.Content)
}
}
Java
The Java package builds on protobuf-java and runs on Java 8 or newer. Message types are in io.github.tommannironclad.sapient.bsiflex335.v2_0, not DSTL's uk.gov.dstl.sapientmsg packages, so the two can share a classpath.
<!-- Maven -->
<dependency>
<groupId>io.github.tom-mann-ironclad</groupId>
<artifactId>sapient-bindings</artifactId>
<version>0.1.0</version>
</dependency>
// Gradle
implementation "io.github.tom-mann-ironclad:sapient-bindings:0.1.0"
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import io.github.tommannironclad.sapient.SapientFraming;
import io.github.tommannironclad.sapient.bsiflex335.v2_0.SapientMessage;
public class FusionNode {
public static void main(String[] args) throws Exception {
try (ServerSocket server = new ServerSocket(5000, 1, InetAddress.getLoopbackAddress());
Socket conn = server.accept();
InputStream stream = conn.getInputStream()) {
while (true) {
SapientMessage msg = SapientFraming.readFrame(stream, SapientMessage.parser());
System.out.println(conn.getRemoteSocketAddress() + ": " + msg.getContentCase());
}
}
}
}
Send with SapientFraming.writeFrame(stream, message).
Schema source and licensing
The bindings are generated from the protobuf schema DSTL publishes in SAPIENT-Proto-Files. The schema files are Crown Copyright under Apache-2.0; the bindings packages are MIT or Apache-2.0, at your option. The schema defines the messages; BSI Flex 335 defines what they mean and how they must be used.