The PJRT Plugin Contract: pjrt_api.cc, Line by Line

A vendor ships a shared library for an accelerator the framework has never been compiled against, and it runs your model anyway. The entire arrangement is one exported C symbol and two integers, and this is the 209-line file where the two sides meet. We read all of it: the global map from a device-type string to a function table, the lowercase that is the whole matching rule, the write-once registration that refuses to let two packages claim one name, and the loader itself, which dlopens a library, asks for a single symbol called GetPjrtApi, and never closes the file again because the table lives inside it. Then the handshake. Where the plugin gets its version number from, why major must match exactly and minor only has a floor, the environment variable that switches the rule from generous to strict, and the value that reads as encouraging while doing the opposite. The signature exhibit runs the real comparison order: set the version the plugin was built against, set the environment variable, and read back the exact error string absl::StrCat assembles, trailing-parenthesis quirk included.

Code walk · AI / ML. The source ↗

A free, interactive, animated visual explainer of The PJRT Plugin Contract: pjrt_api.cc, Line by Line — built to be understood, not skimmed.

Questions

What is a PJRT plugin?
A shared library on disk that implements the PJRT C API for one kind of accelerator. It exports exactly one function by name, GetPjrtApi, taking no arguments and returning a pointer to a PJRT_Api struct. That struct opens with a size, an extension list head and a two-integer version, and then holds 138 function pointers covering everything a framework can ask a device to do: create a client, compile a program, put a buffer on the device, execute, read the buffer back. The boundary is plain C rather than C++ on purpose. C++ has no stable binary interface, so two builds from different compilers cannot safely call each other, while a C struct of function pointers can be written by one toolchain and read by another. That is what lets a vendor ship a plugin built at a different time, by a different team, against a different standard library than the framework that loads it.
What symbol does a PJRT plugin have to export?
One, named GetPjrtApi, with the C signature const PJRT_Api* GetPjrtApi(). The loader in pjrt_api.cc dlopens the library with RTLD_LAZY, calls dlsym for that exact string, and returns a NotFound error naming your library path if it is absent. Nothing else is looked up by name, ever. The in-tree CPU plugin shows both halves of the contract in two short files: pjrt_c_api_cpu.cc defines GetPjrtApi as a single return statement, and pjrt_c_api_cpu_version_script.lds is a linker script whose global section lists that one symbol and marks everything else local, so a framework that opens the library can see nothing but the entry point. On a successful load the library is never dlclosed, because the table it returned, and every function that table points at, live inside it.
What does ENABLE_PJRT_COMPATIBILITY do?
It picks which of two version rules InitializePjrtPlugin applies. With compatibility on, which is the default, the plugin major version must equal the framework major version and the plugin minor version must be at least kMinPjRtMinor, currently 29, with no upper bound at all. With compatibility off, both numbers must match the framework exactly. The reading of the variable has a trap in it worth knowing. If it is unset, getenv returns null and the function returns true, so the default is the permissive rule. If it is set, the value goes through absl::SimpleAtob, which accepts only true, t, yes, y, 1, false, f, no, n and 0, case-insensitively. Any other spelling is a parse failure, and a parse failure is turned into false. So ENABLE_PJRT_COMPATIBILITY=on switches compatibility off and puts the framework into exact-match mode.
Why does my plugin fail with a mismatched PJRT API version error?
There are three different messages and they mean different things. A mismatched major version is unconditional: the major number is bumped only when the memory layout changes, so nothing can be salvaged and no setting overrides it. A message saying your plugin version is older than the minimum supported version means the plugin minor is below 29, the floor set once in October 2023 and never raised. A message about a mismatched version with both numbers in it, rather than a mismatched major version, means you are in exact-match mode, which happens when ENABLE_PJRT_COMPATIBILITY is set to a false value or to a spelling the bool parser cannot read. Before any of them, line 168 logs both version numbers unconditionally, and that log line is the fastest way to see whether the library you loaded is the one you thought you loaded.
Can a PJRT plugin be newer than the framework that loads it?
Yes, and it is a supported case rather than an accident. Under the default rule the only tests are major equality and minor being at least the floor, so a plugin reporting 0.115 against a framework at 0.114 passes both, and so does one reporting 0.200. The C ABI is designed for that direction: every struct carries its own struct_size, so the newer side can work out how much of the older side exists, and the framework simply never reads the fields it does not know about. What catches a genuine incompatibility is a separate, per-call check comparing those sizes, whose error says the plugin is likely built with a later version than the framework. That failure happens at the specific call needing the newer field rather than shutting the plugin out at load time, which is why a newer plugin usually just works for everything you actually use.

Related explainers