/capnp-swift

(Unofficial, WIP) Cap'n Proto support for Swift; serialization only.

Primary LanguageSwiftMozilla Public License 2.0MPL-2.0

Cap'n Proto Swift

Cap'n Proto runtime and code generation for Swift. Not an official Cap'n Proto project.

Features

  • 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.

Example

The addressbook.capnp example is available in Tests/CapnProtoTests/AddressBook.swift.

Safety

The library and generated code is designed to handle malicious inputs, but it is not 100% there:

  1. 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.

  2. The traversal limit which prevents amplification attacks is not implemented. Only the pointer depth limit which prevents stack overflows is implemented.

  3. There are few tests, and the code has not undergone any thorough review.

Usage

Using plugins

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"),
      ]
    ),
  ]
)

Manually

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.capnp

Note

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/debug

Design

Unlike 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).

APIs

  • 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. EnumValue provides access to the underlying value as E? or UInt16.

  • Unions are represented as structs where all union fields are Optional, and with additional methods and types:

    • var whichDiscriminant: EnumValue<Which.Discriminant> returns the raw discriminant of the union.

    • func which() -> Which? returns a Swift enum wrapping the union data, or nil if the discriminant is for an unknown field. It can throw if one of the fields is a pointer.

Multithreading

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:

  1. Both the Message and all pointer types (List, Struct) store a "mutable bit", so determining if a Struct is mutable does not require dereferencing its Message.

    This mutable bit takes no space at all; it is stored in the traversal limit counter.

  2. 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.

Development

After modifying a .capnp file in this repository, run:

Tools/compile-proto.sh