team-unstablers/swift-msquic
A Swift wrapper for [MsQuic](https://github.com/microsoft/msquic), providing prebuilt binaries and an idiomatic Swift API with async/await support.
Features
- Swift Concurrency Support: All asynchronous operations (connect, send, receive, etc.) are wrapped with
async/awaitandAsyncSequence. - Memory Safety: Class-based wrappers handle MsQuic handle lifetimes automatically using ARC (Automatic Reference Counting).
- Prebuilt Binaries: Includes
MsQuic.xcframework(v2.5.6-tuvariant), so you don't need to build MsQuic from source. - iOS Compatible: Modified to comply with iOS App Store guidelines (removed
dlopencalls). - Stream Scheduling Controls: Supports connection-level stream scheduling (
fifo/roundRobin) and per-stream priority.
Requirements
- Swift 5.9+
- macOS 13.0+
- iOS 16.0+
Installation
Add swift-msquic to your Package.swift dependencies:
dependencies: [
.package(url: "https://github.com/team-unstablers/swift-msquic.git", from: "1.1.3")
]Then add SwiftMsQuicHelper to your target dependencies:
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "SwiftMsQuic", package: "swift-msquic")
]
)
]Usage
1. Initialize API
You must initialize the MsQuic API before using it.
import SwiftMsQuicHelper
// Initialize
try SwiftMsQuicAPI.open().throwIfFailed()
// Cleanup when done
defer { SwiftMsQuicAPI.close() }2. Client Example
func runClient() async throws {
// 1. Create Registration & Configuration
let reg = try QuicRegistration(config: .init(appName: "MyClient", executionProfile: .lowLatency))
let config = try QuicConfiguration(registration: reg, alpnBuffers: ["my-proto"])
// Disable certificate validation for testing (NOT for production)
try config.loadCredential(.init(type: .none, flags: [.client, .noCertificateValidation]))
// 2. Connect
let connection = try QuicConnection(registration: reg)
try await connection.start(configuration: config, serverName: "localhost", serverPort: 4567)
// Optional: use round-robin scheduling across streams of the same priority
try connection.setStreamSchedulingScheme(.roundRobin)
// 3. Open Stream & Send Data
do {
let stream = try connection.openStream(flags: .none)
try await stream.start()
try stream.setPriority(0x9000) // 0xFFFF is highest priority
try await stream.send(Data("Hello".utf8), flags: .fin)
// 4. Receive Data
for try await data in stream.receive {
print("Received: \(String(decoding: data, as: UTF8.self))")
}
await stream.shutdown(flags: .graceful)
}
// 5. Shutdown Connection
await connection.shutdown()
}Locally opened streams should be released before you expect transport resources to be fully closed. await connection.shutdown() waits for the transport shutdown event, but ConnectionClose still happens from deinit.
3. Server Example
func runServer() async throws {
let reg = try QuicRegistration(config: .init(appName: "MyServer", executionProfile: .lowLatency))
// Configure settings (e.g., timeouts, peer stream counts)
var settings = QuicSettings()
settings.peerBidiStreamCount = 100
settings.idleTimeoutMs = 30000
let config = try QuicConfiguration(registration: reg, alpnBuffers: ["my-proto"], settings: settings)
// Load Server Certificate
try config.loadCredential(.init(
type: .certificateFile(certPath: "server.crt", keyPath: "server.key"),
flags: []
))
let listener = try QuicListener(registration: reg)
// Handle new connections
listener.onNewConnection { listener, info in
let connection = try QuicConnection(handle: info.connection, configuration: config) { conn, stream, flags in
// Handle new streams
do {
for try await data in stream.receive {
// Echo back
try await stream.send(data)
}
await stream.shutdown(flags: .graceful)
} catch {
print("Stream error: \(error)")
}
}
return connection
}
try listener.start(alpnBuffers: ["my-proto"], localAddress: QuicAddress(port: 4567))
// Keep the server running...
try await Task.sleep(nanoseconds: 100_000_000_000_000)
}4. Stream Scheduling & Priority
QuicConnection supports connection-level stream scheduling:
try connection.setStreamSchedulingScheme(.fifo) // default
try connection.setStreamSchedulingScheme(.roundRobin) // fairness for same-priority streams
let scheme = try connection.getStreamSchedulingScheme()
print("Current scheme: \(scheme)")QuicStream supports per-stream send priority (UInt16, 0x0000...0xFFFF):
try stream.setPriority(0xFFFF) // highest
let priority = try stream.getPriority()
print("Current stream priority: \(priority)")Debug Build
This package ships both Release and Debug (with MsQuic internal logging enabled) prebuilt binaries. By default, the Release binary is used.
To switch to the Debug binary, set the MSQUIC_DEBUG environment variable before building:
MSQUIC_DEBUG=1 swift buildNote: This environment variable is evaluated at package resolution time (
Package.swift), not at build time. Xcode resolves packages through its own process, so this method works reliably only with the Swift CLI (swift build,swift test, etc.).
Important Notes
- MsQuic Version: The included binary is based on MsQuic v2.5.6.
- Use SwiftMsQuicHelper: It is strongly recommended to use the
SwiftMsQuicHelpermodule instead of importingMsQuicdirectly. Swift's C Interop does not fully support C macros, making it impossible to access MsQuic status codes (which are macros) directly.SwiftMsQuicHelperprovides proper Swift wrappers (e.g.,QuicStatus) to handle this. - Modifications: This repository uses a fork of MsQuic maintained by Team Unstablers Inc. with the following change:
- Removed dlopen(3) calls in quic_bugcheck to ensure compliance with iOS App Store review guidelines.
'Vibe Coding' Notice
Part of this wrapper code was written via "Vibe Coding" using Large Language Models. The following agents/models were used:
- Claude Code: Claude Opus 4.5
- OpenAI Codex: gpt-5.2-codex (xhigh)
- Google Gemini CLI: Google Gemini 3 Pro (Preview)
Package Metadata
Repository: team-unstablers/swift-msquic
Default branch: main
README: README.md