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.

SAPIENT binding packages
LanguagePackageInstall
Rustsapient-rs on crates.iocargo add sapient-rs
Pythonsapient-py on PyPIpip install sapient-py
C#Sapient.Bindings on NuGetdotnet add package Sapient.Bindings
Gogo/sapient on pkg.go.devgo get github.com/tom-mann-ironclad/sapient-bindings/go/sapient/...
Javasapient-bindings on Maven Centralio.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.