The C++ binding¶
C++ writes against the ABI header directly. There is no SDK layer between you and
sdk/abi.h, because on this side there is nothing for one to do: the header is already C,
already the contract, and already the thing every other binding mirrors.
That is the trade. Rust gives you Result, RAII and a checked call for every slot. C++
gives you the contract with nothing in the way, and the four things the SDK would have
done are yours to do:
| Rust | C++ | |
|---|---|---|
| Entry point | generated by a macro | you write pier_main |
| Capability check | inside every wrapper | you compare against struct_size at each call site |
| Strings | &str conversions |
you build PierStr yourself |
| Errors | Result<T> |
a bool return and an out-parameter |
Pick C++ when the mod is mostly engine work and the ABI is a small part of it, or when it has to sit in a codebase that is already C++. Pick Rust when the ABI is most of what the mod does.
What building one takes¶
One header, no library, no build system:
| Text Only | |
|---|---|
examples/hello-pier-cpp in the repository is that mod, with four build files beside it β
MSVC, clang-cl, CMake and xmake β producing the same DLL. Use whichever your project
already uses; the source does not change between them.
The compiler has to produce an MSVC-ABI x64 binary. On Windows that means cl or
clang-cl. A GNU-driver clang++ or MinGW g++ compiles and links and then cannot be
loaded, which is a slow way to find out.
The smallest mod¶
Three things in there are not optional.
PIER_MAIN_EXPORT, not a bare extern "C". Declaring the symbol does not export it. A
DLL exports only what is marked, so a plain declaration builds cleanly and is then refused
at load with "does not export pier_main".
The four header scalars. The host reads struct_size to know how much of your table
it may touch, which is the mirror of what you do to its table. Leaving them zero means the
host sees a table of length zero.
mod_flags names the target. 0 for a server mod, PIER_FLAG_CLIENT for one meant
for the client host. The host compares bit 0 with its own and refuses a mismatch at load,
so a DLL built for one target says so instead of failing slot by slot on the other.
log is called without a capability check, and only log is. It is one of four slots
in the fixed header that every host has. Everything past that header needs the check
below.
Checking a capability before calling it¶
PierApi grows by appending. A host older than the header you compiled against has a
shorter table that simply ends earlier, so reading a slot past its end reads memory the
host never wrote.
Two different reasons a slot is unusable, and both are normal:
- past
struct_sizeβ the host predates this capability. - NULL β the host is new enough but was built without the package that fills the slot.
pier-dimensionsandpier-laneare droppable, and a client build fills a different set. A NULL slot is not a bug to report; it is a capability to do without.
tools/pier-probe is a mod that reports both, per slot, against a running host.
Strings¶
PierStr is {pointer, length} and is not NUL-terminated. A string handed to your
callback belongs to the caller and is valid only for that call: copy it if you want to
keep it.
| C++ | |
|---|---|
A mod hands strings back through a sink callback within the current call frame. Ownership never crosses the boundary in either direction; there is no call in this ABI that returns a pointer the other side must free.
Errors¶
A slot that can fail returns bool, and the value goes into an out-parameter. False means
the host has no answer, and it does not distinguish "not found" from "this engine version
does not expose that property". Both are results, not errors, and neither is logged by the
host.
False and a zero result are different answers. Collapsing them is the mistake contract Β§5.2 exists to name.
Exceptions¶
Every function the host calls is a C function: pier_main, the lifecycle callbacks, and
every event, command, task, service and sink callback. An exception must not leave any of
them. Unwinding into the host's frames is undefined behavior; with MSVC's /EHsc the
host is compiled on the assumption that a C function never throws.
The host catches what it can around pier_main and the lifecycle callbacks and treats it
as a refusal, but that is a last fence, not a contract. Anything that allocates, parses or
calls into a library that throws needs its own try at the top of the callback:
| C++ | |
|---|---|
Marking the callback noexcept instead is not the same thing: an exception reaching it
calls std::terminate, which takes the whole server down instead of one request.
Hangs¶
A callback runs on the host's thread, usually the server thread. If it does not return,
the server stops ticking. Pier's watchdog names the mod and the entry it is stuck in after
watchdog.warn_ms, and ends the process after watchdog.hang_ms so a supervisor can
restart the server; see Configuring. It cannot stop
just your mod: there is no safe way to pull a thread out of code that holds locks.
Blocking work β network, disk, long computations β belongs on a thread of your own, with
the result handed back through schedule.
Where to go¶
- Your first mod β from an empty directory to a DLL the server loads.
- The ABI β the contract itself, which on this side you read directly.
- The Rust binding β the same capabilities with the four columns above filled in for you.