relic_native

A Relic adapter backed by a Zig HTTP server (zio and std.http.Server) that each isolate drives from its own thread.

An acceptor thread hands each connection to the isolate with the fewest. From there the parsing, the keep-alive handling and the timeouts run in native code between the isolate's Dart work, and a request reaches the handler as plain Dart data. The adapter is opt-in: app.serve() stays on relic_io, and this package is used through serveNative.

import 'package:relic_core/relic_core.dart';
import 'package:relic_native/relic_native.dart';

Future<void> main() async {
  final app = RelicApp()..get('/', (final req) => Response.ok());
  await app.serveNative(port: 8080, noOfIsolates: 4);
}

HTTPS

Pass a NativeTls with the certificate chain and private key as PEM, and the server speaks HTTPS. tls.zig runs the handshake and the records in native code, on the same thread as the rest of the connection.

await app.serveNative(
  port: 8443,
  tls: NativeTls.fromFiles(
    certificateChain: 'cert.pem',
    privateKey: 'key.pem',
  ),
);

The chain holds the server's certificate first, then any intermediates. The key is a PKCS#8 key, ECDSA, RSA or Ed25519, or a SEC 1 or PKCS#1 one, the EC PRIVATE KEY and RSA PRIVATE KEY blocks cert-manager writes by default. An encrypted key takes its password: PKCS#8 under PBES2 with PBKDF2 and AES-128-CBC or AES-256-CBC, or a key block behind OpenSSL's DEK-Info headers with the same ciphers, as openssl pkcs8, openssl genpkey and openssl ec -aes256 write them. An empty password counts as none. bind throws a TlsException for a key it cannot use, for a missing or wrong password, and for a chain with no certificate or one that is expired, unreadable or repeats a subject. The server speaks TLS 1.3 only and selects http/1.1 through ALPN. A client whose ALPN offer holds no http/1.1 is refused with a TLS alert. A client that sends plain HTTP to the port gets a plain 400.

To require a client certificate, pass the CA certificates it must chain to as clientAuthorities:

tls: NativeTls.fromFiles(
  certificateChain: 'cert.pem',
  privateKey: 'key.pem',
  clientAuthorities: 'client_ca.pem',
),

A client without one, or with one that does not chain to them, is refused with a TLS alert. A handler does not see the certificate, only that the connection passed the check. bind throws a TlsException for client authorities with no certificate, or one that is expired, unreadable, not a CA, that is, without the v3 basicConstraints that marks one, or repeats a subject.

Platforms

Linux (x64, arm64), macOS (arm64, x64) and Windows (x64, arm64). On another platform the package builds no native library, and NativeAdapter.bind throws. Use relic_io there.

The prebuilt libraries are compiled for CPUs with hardware AES, which TLS needs to run at speed. On x64 that is AES-NI, PCLMULQDQ and AVX, which server and desktop CPUs of the last decade have, apart from some low-end Atom, Celeron and Pentium models. On Linux and Windows arm64 it is the ARMv8 crypto extensions, which the Raspberry Pi 4 lacks. On such a CPU the library stops with an illegal instruction. Use relic_io there too.

Building from source

The published package ships prebuilt libraries. A source checkout compiles with Zig through the build hook. The zig on PATH must be the version in build.zig.zon, or anyzig, which picks it from that file.

Isolates

Each isolate runs its own reactor on its own thread: the sockets, the parsing and the writes of the connections it was handed happen between its Dart work, in ticks the adapter drives from the event loop. One acceptor thread per server hands each new connection to the isolate with the fewest. noOfIsolates is the one knob, and a trivial route scales with it up to the machine's cores.

Choosing between relic_io and relic_native

relic_io is the default. app.serve() uses it and the relic package does not depend on relic_native. Both run the same handlers, middleware and router, and both pass the same conformance suite. The differences:

  • Throughput and tail latency. On a plaintext route the native adapter answers several times the requests per second of relic_io on one isolate, and its p99 under load is lower, since parsing and keep-alive run on native threads while the isolate is busy. The numbers are in doc/design/relic_native/NOTES.md.
  • TLS. Both terminate it. relic_io uses a SecurityContext and speaks TLS 1.2 and 1.3. relic_native takes PEM through NativeTls and speaks TLS 1.3 only.
  • Platforms. relic_io runs wherever Dart does. relic_native runs on Linux, macOS and Windows.
  • Dependencies. relic_io is pure Dart. relic_native ships prebuilt libraries and needs Zig only for a source checkout.

Stay with relic_io unless you have measured the dart:io adapter as the bottleneck. relic_native is newer code with a native library in the process, and the gain only matters once the server is the limit.

Limits

HTTP/1.0 and HTTP/1.1 only, with Content-Length and chunked bodies both ways. No HTTP/2 or HTTP/3. TLS 1.3 only, see above. A handler can hijack the connection as a raw byte channel, and a WebSocket upgrade is framed by the native side, so capabilities.hijack and capabilities.webSocket are both true. A WebSocket message is at most maxWebSocketMessage bytes, 16 MiB unless bind is told otherwise. A request head is at most 16 KiB with at most 128 fields, and a body over maxInlineBody streams to the handler instead of being read first. Under TLS a connection holds about 33 KiB more, the buffers a record needs.

Libraries

relic_native
A Relic adapter backed by a Zig HTTP server each isolate drives from its own thread.