swift.md

August 24, 2026 ยท View on GitHub

Fory can generate Swift gRPC service companions for schemas that define services. The companion provides the usual gRPC service providers, clients, method descriptors, and service metadata, while request and response objects are serialized with Fory instead of protobuf.

Use this mode when both RPC peers are generated from the same Fory IDL, protobuf IDL, or FlatBuffers IDL and both sides expect Fory-encoded message bodies. Use normal protobuf gRPC generation for APIs that must be consumed by generic protobuf clients, reflection tools, or components that expect protobuf bytes.

The companion targets grpc-swift 1.x. That line keeps the same platform floor as the Fory Swift package (macOS 13, iOS 16); grpc-swift 2.x requires a newer floor.

Add Dependencies

The Fory package does not depend on grpc-swift. Add grpc-swift in the package that compiles or runs the generated companions:

// Package.swift
dependencies: [
    .package(url: "https://github.com/apache/fory.git", exact: "$version"),
    .package(url: "https://github.com/grpc/grpc-swift.git", from: "1.23.0"),
],
targets: [
    .target(
        name: "App",
        dependencies: [
            .product(name: "Fory", package: "fory"),
            .product(name: "GRPC", package: "grpc-swift"),
        ]
    )
]

Define a Service

Service definitions can come from Fory IDL, protobuf IDL, or FlatBuffers rpc_service definitions. A Fory IDL service looks like this:

package demo.greeter;

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string reply = 1;
}

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

Generate Swift model and gRPC companion code with --grpc:

foryc service.fdl --swift_out=./Sources/App --grpc

For this schema the Swift generator emits:

FilePurpose
demo/greeter/greeter.swiftFory model types and the ForyModule helper
demo/greeter/GreeterGrpc.swiftgRPC providers, client, and service metadata

Generated gRPC symbols are prefixed with the package, so the schema above emits Demo_Greeter_GreeterAsyncProvider, Demo_Greeter_GreeterAsyncClient, and Demo_Greeter_GreeterProvider. A schema with no package drops the prefix (GreeterAsyncProvider).

Implement a Server

Conform a type to the generated async/await provider and host it with a normal grpc-swift Server:

import Fory
import GRPC
import NIOPosix

final class GreeterService: Demo_Greeter_GreeterAsyncProvider {
  func sayHello(
    request: Demo.Greeter.HelloRequest,
    context: GRPCAsyncServerCallContext
  ) async throws -> Demo.Greeter.HelloReply {
    Demo.Greeter.HelloReply(reply: "Hello, " + request.name)
  }
}

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let server = try await Server.insecure(group: group)
  .withServiceProviders([GreeterService()])
  .bind(host: "127.0.0.1", port: 1234)
  .get()

Request and response types are registered by the generated schema module that the companion uses, so server code does not register serializers by hand. An EventLoopFuture-based Demo_Greeter_GreeterProvider is also emitted for servers that do not use async/await.

Create a Client

Use the generated async client over a grpc-swift channel:

import Fory
import GRPC
import NIOPosix

let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let channel = try GRPCChannelPool.with(
  target: .host("127.0.0.1", port: 1234),
  transportSecurity: .plaintext,
  eventLoopGroup: group)

let client = Demo_Greeter_GreeterAsyncClient(channel: channel)
let reply = try await client.sayHello(Demo.Greeter.HelloRequest(name: "Fory"))
print(reply.reply)

Streaming RPCs

Fory service definitions can use the four gRPC streaming shapes:

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc LotsOfReplies (HelloRequest) returns (stream HelloReply);
  rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply);
  rpc BidiHello (stream HelloRequest) returns (stream HelloReply);
}

Streaming methods present clean request and response types. The provider receives a response writer (send(_:)) for server output and an AsyncSequence for client input; the client returns an AsyncSequence of responses for server-streamed replies:

// Server side
func lotsOfReplies(
  request: Demo.Greeter.HelloRequest,
  responseStream: Demo_Greeter_GreeterAsyncResponseStream<Demo.Greeter.HelloReply>,
  context: GRPCAsyncServerCallContext
) async throws {
  try await responseStream.send(Demo.Greeter.HelloReply(reply: "Hi " + request.name))
}

// Client side
for try await reply in client.lotsOfReplies(Demo.Greeter.HelloRequest(name: "Fory")) {
  print(reply.reply)
}

gRPC Runtime Behavior

Generated companions carry Fory-encoded bytes inside a private GRPCPayload wrapper. The Swift Fory instance is single-threaded, so the wrapper uses one Fory per thread, built from the schema module's configuration and registrations, which makes concurrent RPCs safe without sharing a single instance. Imported request and response types resolve to their own namespace and are registered transitively through the owning module, so a service that crosses an import boundary works without extra registration.

Swift Language Mode

Compile generated companions in Swift 5 language mode (use swift-tools-version:5.9, or set swiftLanguageMode(.v5) on the target in a 6.x manifest). grpc-swift moves each request and response between the calling task and the event loop, so the wire wrapper requires a Sendable payload, and generated Fory Swift models do not declare that conformance. This applies to every call shape, including unary calls, not only the streaming ones.

Known Limitations

The generated client is async/await only. grpc-swift's EventLoopFuture client returns call objects parameterized by the on-the-wire message type, which would expose the internal Fory wrapper, so it is not emitted. Both providers (async and EventLoopFuture) are generated.

Interceptors are not generated. grpc-swift interceptors are typed on the on-the-wire message, which is the internal Fory wrapper; emitting interceptor hooks would expose that wrapper. Use a custom channel or server configuration for cross-cutting concerns instead.

RPC names must produce a usable Swift member. The compiler rejects an rpc whose name is only underscores, because it normalizes to _, which Swift reserves for discards. It also rejects handle, serviceName, channel, and defaultCallOptions, which collide with members of the generated provider and client. Rename the rpc in the schema.

Swift models put each package under a nested enum namespace, so two schemas that share a top-level package component (for example demo.shared and demo.greeter) both emit public enum Demo. The compiler rejects that with a top-level symbol collision before it writes either file. This is a model-generation behavior, not specific to gRPC, but it also affects a service that imports across such packages. Give the schemas disjoint top-level packages (for example shared.models and greeter.api). Generating into separate Swift modules with one foryc invocation each only helps unrelated schemas, because the preflight collects imports recursively: compiling demo.greeter still includes demo.shared in the graph and rejects the duplicate Demo even if the shared schema was generated in another invocation. An import graph needs disjoint top-level packages.

Troubleshooting

Missing grpc-swift Types

If the build cannot find GRPCAsyncServerCallContext, Server, or GRPCChannelPool, add the grpc-swift dependency and the GRPC product to the target that compiles the generated companion.

Protobuf Clients Cannot Decode the Service

Generated companions exchange Fory-encoded bodies, not protobuf bytes. A generic protobuf client cannot decode them. Both peers must be generated from the same Fory IDL and use the generated Fory companions.