Skip to content

Platforms and languages

Five platforms. One C ABI. Every binding printed from it.

macOS, iOS, Android, Windows and Linux, from Rust, C, Swift, Kotlin and Java, .NET, Python, Dart and React Native. Application mode, with PCM in your hands, works on every one of them.

Where it runs

Device mode is wherever the library has an audio backend. The artefacts are built by the release scripts; none is published to a package registry before 1.0.

Platforms, languages and device mode
Platform Languages Device mode Artefact
macOS RustCSwiftKotlin/Java.NETPythonDart CoreAudio voice processing XCFramework, NuGet, wheel
iOS RustCSwiftReact NativeFlutter CoreAudio voice processing, CallKit and PushKit helpers XCFramework
Android RustCKotlinReact NativeFlutter AAudio (API 28+), ConnectionService helper AAR for arm64-v8a, armeabi-v7a, x86_64
Windows RustC.NETPythonDart WASAPI communications streams NuGet, wheel
Linux RustCKotlin/Java.NETPythonDart application mode; sipral-io-pipewire for a desktop NuGet, manylinux wheels, JVM jar (x64, arm64)

Application mode, PCM in the application's hands, is available on every platform and in every language.

The same call, four languages

Every language gets an idiomatic layer of the same shape: a stack, an account, a call, and a stream of events. The examples come from each binding's own documentation.

The lab runs a Python, a Swift, a Kotlin and a .NET agent against Asterisk on every run of the interoperability matrix.

call.py python
import asyncio
from sipral import Stack
from sipral.enums import RegistrationState

async def main():
    with Stack(loop=asyncio.get_running_loop()) as stack:  # device mode: the library opens mic and speaker
        account = stack.add_account(
            "sip:alice@example.com", registrar="sip:example.com",
            server_uri="sip:example.com",  # located by RFC 3263: NAPTR, SRV, A/AAAA
            auth_user="alice", auth_password="secret")
        account.register()
        while account.registration_state != RegistrationState.REGISTERED:
            await stack.events.get()
        call = stack.place_call(account, "sip:bob@example.com")
        while not call.ended:
            print((await call.events.get()).kind_name)

asyncio.run(main())
From the Python binding's documentation.
Call.swift swift
import Sipral

let stack = try SipralStack(bindHost: "192.0.2.10")
let account = try stack.addAccount(
    aor: "sip:alice@example.invalid",
    registrarAddress: "203.0.113.10:5060",
    registrar: "sip:example.invalid",
    authUser: "alice",
    authPassword: secret
)
try account.register()

// Without `registrar` the account never registers: registering throws, and
// the registrar address is only the outbound proxy. Left out, `bindHost` and
// `mediaHost` are the address of the route toward the registrar.

let call = try stack.placeCall(account: account, target: "sip:bob@example.invalid", mediaHost: "192.0.2.10")
let events = call.events()                 // take it at once: nothing earlier is replayed
for await event in events {
    if event.kind == .mediaStarted { break }
}
// the microphone and the loudspeaker are the library's from here on: the
// stack is in device mode, and the call is heard without a line of audio code

try call.hold()
try call.resume()
try call.sendDtmf("123#")
try call.transfer(to: "sip:carol@example.invalid")   // blind: .transferDone's transferData says how it went
try call.hangup(reason: .normalClearing)   // or hangup(), with no Reason
call.close()
From the Swift binding's documentation.
Call.kt kotlin
val client = SipralClient.open(bindHost = "192.0.2.10")
val account = client.addAccount(
    "sip:alice@example.com",
    registrarAddress = "203.0.113.5:5060",
    registrar = "sip:example.com",
    authUser = "alice",
    authPassword = secret,
)
account.registerAndWait()
val call = client.placeCall(account, "sip:bob@example.com", mediaHost = "192.0.2.10")
call.waitConfirmed()        // heard at once: the client is in device mode wherever the library has an engine
call.hold(); call.resume()
call.sendDtmf("123#")
call.transfer("sip:carol@example.com")   // blind: TRANSFER_DONE, read with transferOf, says how it went
call.hangup(SipralHangupReason.NORMAL_CLEARING)   // or hangup(), with no Reason
client.close()
From the Kotlin binding's documentation.
Call.cs csharp
using Sipral;

using var stack = new SipralStack(bindHost: "192.0.2.10");
var account = stack.AddAccount(
    "sip:alice@example.invalid",
    registrarAddress: "203.0.113.10:5060",
    registrar: "sip:example.invalid",
    authUser: "alice",
    authPassword: secret);
account.Register();

// Without `registrar` the account never registers: registering throws,
// and the registrar address is only the outbound proxy. Left out,
// `bindHost` and `mediaHost` are the address of the route toward the
// registrar.

var call = stack.PlaceCall(account, "sip:bob@example.invalid", mediaHost: "192.0.2.10");
await call.WaitForMediaAsync();

call.Hold();
call.Resume();
call.SendDtmf("123#");
call.Hangup();
From the .NET binding's documentation.

The bindings

The header and every binding are printed from the declarations in the FFI crate, and the release gate prints them again and fails if what is committed is not what came out. A function added on the Rust side and missing from a binding is a failed check rather than a crash on one platform.

BindingWhere it runsAudioWhat you get
C anywhere the library builds device or application mode one header for any language with a C FFI
Swift macOS, iOS device or application mode; CallKit and PushKit helpers Swift classes and async event streams
Kotlin and Java Android (AAR), a JVM (jar for linux-x64 and linux-arm64) device mode on Android from API 28, application mode anywhere; a ConnectionService helper Kotlin coroutines, a Java face for server JVMs
.NET Windows, macOS, Linux device or application mode a C# API with async/await
Python macOS, Windows, Linux device or application mode an asyncio API
Dart and Flutter wherever dart:ffi loads the library application mode, signalling over UDP a Dart API over dart:ffi
React Native iOS, Android device mode, over the Swift and Kotlin layers a TurboModule over the Swift and Kotlin layers

An ABI meant to be frozen

Frozen at 1.0
names, numbers, layouts and ownership rules
Growth
every later version only appends: every field keeps its offset
Layouts
checked on 64-bit and both 32-bit ABIs, compiled for six targets
Loading
a mismatched header is refused at load, not discovered as a crash
sipral.h, excerpt c
#define SIPRAL_ABI_VERSION_MINOR ((uint32_t)35)
typedef uint64_t sipral_handle_t;

sipral_status_t sipral_stack_create(const sipral_stack_config_t *config, sipral_handle_t *out_stack);
sipral_status_t sipral_stack_poll(sipral_handle_t stack, uint64_t now_ms, sipral_poll_result_t *result);
sipral_status_t sipral_account_add(sipral_handle_t stack, const sipral_account_config_t *config, sipral_handle_t *out_account);
sipral_status_t sipral_account_register(sipral_handle_t stack, sipral_handle_t account, uint64_t now_ms);
sipral_status_t sipral_call_place(sipral_handle_t stack, sipral_handle_t account, const sipral_call_config_t *config, sipral_handle_t *out_call, uint64_t now_ms);
sipral_status_t sipral_media_receive(sipral_handle_t media, uint8_t *data, size_t len, const char *from, size_t from_len, uint64_t now_ms, sipral_arrival_t *out_arrival);
sipral_status_t sipral_media_playback(sipral_handle_t media, int16_t *samples, size_t capacity, size_t *out_written, sipral_playback_t *out_source);
sipral_status_t sipral_call_hangup(sipral_handle_t stack, sipral_handle_t call, uint64_t now_ms);
One C header; every binding is generated from it.

Ship SIP in a closed app, without the GPL.

One price per company, perpetual for the versions delivered during maintenance, app stores allowed. Ask privately.

Request a quote