Platforms and languages
Five platforms. One C ABI. Every binding printed from it.
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.
| 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.
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()) 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() 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() 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(); 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.
| Binding | Where it runs | Audio | What 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
#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); 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