Services (RPC)
A Service is a handwritten host capability behind a shared C# contract. Annotate an interface with
[RpcService] and the DotBoxD.Services.SourceGenerator emits, at compile time:
- a typed client proxy (calls marshal over the wire, no runtime reflection),
- a server dispatcher, and
Provide{Service}/Get<TService>()wiring extensions.
The runtime is peer-based and bidirectional (RpcPeer / RpcHost): one connection can both serve
and call services. It is transport- and codec-neutral:
- Transports:
DotBoxD.Transports.Tcp,DotBoxD.Transports.NamedPipes(and an in-process channel for tests). Channels carry framed messages and know nothing about services. - Codecs:
DotBoxD.Codecs.MessagePack.
The Services + channel + codec libraries target netstandard2.1. Unity/IL2CPP additionally requires
generated MessagePack DTO formatters and a static resolver; target framework compatibility alone does
not prove an AOT build. NativeAOT/IL2CPP hosts must also directly reference the generated
DotBoxD.Services.Generated.DotBoxDGenerated type (for example, read its Services property during
startup) so trimming cannot remove a registry that would otherwise only be found by name.
Why Services (RPC)?
Section titled “Why Services (RPC)?”The problem it solves: interop without hand-written marshaling. Classic RPC makes you build a request envelope, serialize args, match each response back to its call, deserialize, and cast - repetitive and easy to get subtly wrong. Schema-first or hand-rolled stubs have a worse failure mode: the client stub and the server handler drift apart (a renamed param, a changed return type) and you only find out at runtime.
The payoff: one C# interface is the single source of truth. Both the proxy and the dispatcher are
generated from that one interface, so they cannot drift - a contract-shape mismatch is a compile error
(DBXS###), not a wire fault. The implementation is just your logic: nothing DotBoxD-specific leaks
into class CatalogService : ICatalogService; the client calls
connection.Get<ICatalogService>().GetUnitPriceAsync("sword") and the generated proxy does all the
marshaling - one method, one remote round-trip.
Grounded aspects:
- AOT / Unity / IL2CPP reach. Proxy/dispatcher lookup goes through a generated registry rather than
an assembly scan. MessagePack’s default resolver chain is reflection-capable, so AOT deployments must
supply generated DTO formatters through
MessagePackRpcSerializer.CreateWithResolverand validate a real IL2CPP/NativeAOT build for their DTO set. They must also root the generated registry explicitly; the repository’s NativeAOT smoke demonstrates that startup step. - Peer-based, bidirectional. Direction is configuration, not type: the same connection can both
ProvideandGet, so the host can call back into a connected plugin over one demuxed read loop - there is no separate client/server class on the hot path. - Transport- and codec-neutral. The same contract runs over named-pipe, TCP, WebSocket, or an
in-process channel with a swappable codec; the generated proxy, dispatcher, and
Provide/Getextensions are identical either way. - A trusted channel, not a sandbox. A provided service is callable by any peer on the channel, so enforce access control at the transport or application layer. The real trust boundary for untrusted author logic is Kernels / Pushdown, not Services.
When to use Services: a discrete, typed request→response you can await; a bounded number of calls
(one method = one round-trip); host↔plugin callbacks on one connection; or when you need Unity/IL2CPP
reach (Services is the most-mature netstandard2.1 surface).
When to prefer another mode: to react to a high-frequency server event but only need a filtered
subset, prefer the event pipeline (RunLocal) - Where /
Select lower to server-side restricted IR (intermediate representation) so only matching, projected values cross the pipe (one-way push, no
round-trips). To collapse a chatty N-call loop over the host’s fine-grained bindings into one
server-side batch, prefer Pushdown - the batch runs as verified, capability-gated,
fuel-metered IR.
Diagnostics from the generator use the DBXS### prefix - see
reference/diagnostics.md.
See also: the annotated GameServer walkthrough for a
guided tour, or the raw GameServer sample under
samples/GameServer, the
Channels (RPC) guide (quick-start, API reference, Unity integration, transports,
performance), pushdown for composing services server-side, and
docs/examples/coverage-gaps.md for service scenarios no longer shown
by maintained samples.