Cap'n Proto runtime and code generation for Swift. Not an official Cap'n Proto project.
-
Encoding and decoding of Cap'n Proto messages.
-
Swift-friendly APIs with
enums and typed errors. -
Zero-copy messages which operate directly on byte buffers.
-
Serialization over streams, including zero-copy (when segments fully fit in input buffers).
The following are not supported:
-
RPC and interfaces.
-
Packing.
-
Generics.
-
Orphans.
-
Reflection.
The addressbook.capnp example is available in
Tests/CapnProtoTests/AddressBook.swift.
The library and generated code is designed to handle malicious inputs, but it is not 100% there:
-
Possible decoding errors (invalid pointers, overflows) are surfaced as Swift errors. However some parts of the implementation do not perform checked arithmetic yet, and need to be updated to safely decode untrusted messages.
-
The traversal limit which prevents amplification attacks is not implemented. Only the pointer depth limit which prevents stack overflows is implemented.
-
There are few tests, and the code has not undergone any thorough review.
If capnp is in your PATH, Cap'n Proto Swift can be used as a plugin which
will automatically convert .capnp files in your source directory.
// Package.swift
let package = Package(
dependencies: [
.package(url: "https://github.com/71/capnp-swift", branch: "main"),
],
targets: [
.target(
name: "MyTarget",
dependencies: [
.product(name: "CapnProto", package: "capnp-swift"),
],
plugins: [
.plugin(name: "CapnProtoPlugin", package: "capnp-swift"),
]
),
]
)If you do not want the build to happen automatically, you can instead use
capnpc-swift as a Cap'n Proto plugin.
First, add a dependency to capnp-swift:
// Package.swift
let package = Package(
dependencies: [
.package(url: "https://github.com/71/capnp-swift", branch: "main"),
],
targets: [
.target(
name: "MyTarget",
dependencies: [
.product(name: "CapnProto", package: "capnp-swift"),
]
),
]
)Then, use capnp compile to generate your code:
capnp compile $(swift package print-capnp-compile) Sources/MyTarget/schema.capnpNote
swift package print-capnp-compile [output-directory] automatically resolves
paths needed to compile .capnp files and generates arguments given to
capnp compile:
$ swift package print-capnp-compile
--output=/path/to/project/.build/arm64-apple-macosx/debug/capnpc-swift-tool --import-path=/path/to/capnp-swift/An even more manual way to do this is to build capnpc-swift and use its
path:
$ swift build --product capnpc-swift --show-bin-path
/path/to/project/.build/arm64-apple-macosx/debugUnlike the C++ and Rust APIs (but like the Go and ECMAScript APIs), there are no distinctive types for reading and writing messages. Instead, the same types are used when reading and writing, and writing can surface "not written" errors. This was deemed okay as writing can always fail, since the message you're working with in memory may not have enough space for the field you're trying to write (if the message was generated by a previous version of the code or canonicalized).
-
Pointer fields are exposed as
throwing methods as decoding can fail, whereas non-pointer fields are exposed as properties. -
Enums are represented as a generated enum type wrapped in an
EnumValue<E>, as their value may be unknown to the program reading them.EnumValueprovides access to the underlying value asE?orUInt16. -
Unions are represented as
structs where all union fields areOptional, and with additional methods and types:-
var whichDiscriminant: EnumValue<Which.Discriminant>returns the raw discriminant of the union. -
func which() -> Which?returns a Swiftenumwrapping the union data, ornilif the discriminant is for an unknown field. It canthrowif one of the fields is a pointer.
-
Synchronizing access to messages when reading or writing would be expensive, so
most Cap'n Proto types are not
Sendable and
instead work on a shared Message instance within a single thread.
Messages, Lists and Structs can be frozen into a Frozen<T> object which
prevents further mutations and is Sendable.
Tracking whether a Message or Struct is mutable could be needlessly
expensive for those who do not care about multithreading, but the cost was
deemed okay for the following reasons:
-
Both the
Messageand all pointer types (List,Struct) store a "mutable bit", so determining if aStructis mutable does not require dereferencing itsMessage.This mutable bit takes no space at all; it is stored in the traversal limit counter.
-
In order to support default values, it is necessary for parts of a message to be immutable anyway.
Note that freezing a type has, like Swift arrays and strings, copy-on-write semantics: if no other object refers to the underlying data, the data is directly frozen. Otherwise, it is first copied.
After modifying a .capnp file in this repository, run:
Tools/compile-proto.sh