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:
| File | Purpose |
|---|---|
demo/greeter/greeter.swift | Fory model types and the ForyModule helper |
demo/greeter/GreeterGrpc.swift | gRPC 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.