<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Dusan Malusev</title>
    <description>The latest articles on Forem by Dusan Malusev (@malusev998).</description>
    <link>https://forem.com/malusev998</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F205284%2Fd1322214-595c-4b8e-aa0a-da79e0993e7a.jpeg</url>
      <title>Forem: Dusan Malusev</title>
      <link>https://forem.com/malusev998</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/malusev998"/>
    <language>en</language>
    <item>
      <title>dlopen, dlsym, and how PHP loads extensions</title>
      <dc:creator>Dusan Malusev</dc:creator>
      <pubDate>Tue, 19 May 2026 17:02:24 +0000</pubDate>
      <link>https://forem.com/malusev998/dlopen-dlsym-and-how-php-loads-extensions-50i7</link>
      <guid>https://forem.com/malusev998/dlopen-dlsym-and-how-php-loads-extensions-50i7</guid>
      <description>&lt;p&gt;Most programs are linked at build time. &lt;code&gt;dlopen&lt;/code&gt; is for everything else: plugin systems, audio engines that load VST modules, language runtimes that pull in extensions, game loops that reload logic without restarting — and GPU drivers. The mechanism is identical across all of them. Four POSIX functions, one opaque handle, and a contract the compiler cannot enforce for you.&lt;/p&gt;

&lt;p&gt;This is that contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  API vs ABI
&lt;/h2&gt;

&lt;p&gt;These two acronyms get conflated constantly. They describe different things, enforced at different times, by different tools.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;API&lt;/strong&gt; (Application Programming Interface) is a source-level contract. It defines what you can call, with what argument types, and what you get back. The compiler enforces it. Pass an &lt;code&gt;int&lt;/code&gt; where a &lt;code&gt;const char *&lt;/code&gt; is expected and you get a type error before a binary exists. Change a function signature in a header and every file including that header fails to compile. The feedback is immediate and impossible to miss.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;ABI&lt;/strong&gt; (Application Binary Interface) is a binary-level contract. It defines how compiled code actually executes a call: which registers carry which arguments, how structs are laid out in memory, and what a function's name looks like in the symbol table after the compiler has processed it. Nothing enforces this automatically. Two translation units can agree at the source level — identical header, identical types, identical function names — and still disagree at the binary level if compiled with different compilers, different compiler versions, different flags, or different target ABIs.&lt;/p&gt;

&lt;p&gt;The gap between them is where &lt;code&gt;dlopen&lt;/code&gt; lives.&lt;/p&gt;

&lt;p&gt;When you link two &lt;code&gt;.o&lt;/code&gt; files together, the linker sees both sides simultaneously. Any ABI inconsistency either resolves cleanly or produces a linker error. When you &lt;code&gt;dlopen&lt;/code&gt; a &lt;code&gt;.so&lt;/code&gt; at runtime, the two sides were compiled separately — possibly years apart, by different people, with different toolchains. The API contract (the header) might be unchanged. The ABI might not be.&lt;/p&gt;

&lt;p&gt;A concrete example: a library ships &lt;code&gt;struct Config { int version; char *name; }&lt;/code&gt;. Everything compiles. Two years later, a field is inserted between &lt;code&gt;version&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;. The function signatures are untouched; the API is compatible if you recompile. But a plugin compiled against the old header has &lt;code&gt;name&lt;/code&gt; at byte offset 4. The new library reads &lt;code&gt;name&lt;/code&gt; from offset 8. No compiler error. No linker error. A pointer read from the wrong address, producing garbage or a crash.&lt;/p&gt;

&lt;p&gt;This is why "we didn't change the API" is not sufficient. The relevant question is whether the ABI changed.&lt;/p&gt;

&lt;p&gt;ABI stability is a harder property than API stability. It requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never reordering struct fields&lt;/li&gt;
&lt;li&gt;never inserting fields between existing ones&lt;/li&gt;
&lt;li&gt;never changing the size of any exported type&lt;/li&gt;
&lt;li&gt;never changing a function signature in ways that affect register assignment&lt;/li&gt;
&lt;li&gt;using the same name mangling scheme — which implies the same language and often the same compiler family&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Linux distributions maintain ABI stability for core system libraries across release cycles. Most application libraries do not, and signal breaks via &lt;code&gt;SONAME&lt;/code&gt; version bumps — &lt;code&gt;libfoo.so.1&lt;/code&gt; → &lt;code&gt;libfoo.so.2&lt;/code&gt; — so the dynamic linker refuses to load the wrong version. &lt;code&gt;dlopen&lt;/code&gt; by bare filename bypasses even that check. You own the contract entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  why dynamic loading exists
&lt;/h2&gt;

&lt;p&gt;A statically linked binary resolves every symbol at link time. Addresses are baked in before the binary touches disk — predictable, fast, and completely inflexible. &lt;code&gt;dlopen&lt;/code&gt; goes further than the normal dynamic linker: not only are the libraries separate from the host binary, the decision of &lt;em&gt;which&lt;/em&gt; library to load and &lt;em&gt;when&lt;/em&gt; happens at runtime, in your code.&lt;/p&gt;

&lt;p&gt;The cost is real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No compile-time type checking across the boundary&lt;/li&gt;
&lt;li&gt;No ABI guarantees by default&lt;/li&gt;
&lt;li&gt;The linker cannot dead-strip code it never sees&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you get in exchange depends on what problem you're solving:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugin systems&lt;/strong&gt; — discover and load behavior the host binary never knew about at build time. A text editor loading syntax highlighters, a game engine loading user mods, an audio DAW loading instrument plugins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hot reloading&lt;/strong&gt; — recompile a &lt;code&gt;.so&lt;/code&gt; while the host process is running, swap the old handle for the new one, and continue with updated logic without restarting. The key design constraint is that &lt;em&gt;state&lt;/em&gt; lives in the host and &lt;em&gt;behavior&lt;/em&gt; lives in the &lt;code&gt;.so&lt;/code&gt;. The host allocates the game world, the simulation state, the in-memory database — whatever must survive across reloads. The &lt;code&gt;.so&lt;/code&gt; contains only the code that operates on it. When the &lt;code&gt;.so&lt;/code&gt; is recompiled, the host calls &lt;code&gt;dlclose&lt;/code&gt; on the old handle and &lt;code&gt;dlopen&lt;/code&gt; on the new one between two iterations of its main loop, re-resolves the function pointers via &lt;code&gt;dlsym&lt;/code&gt;, and continues. The state was never in the &lt;code&gt;.so&lt;/code&gt;, so nothing is lost. This is a development workflow rather than a production deployment strategy, but it eliminates the restart-and-reproduce cycle for anything with a slow startup — a game engine loading assets, a simulation with expensive initialization, a server with a warm cache. Tsoding has demonstrated this pattern repeatedly on stream: game logic in a &lt;code&gt;.so&lt;/code&gt;, main loop polling for mtime changes, swap between frames, state intact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Language runtimes&lt;/strong&gt; — PHP, Python, Ruby, and Lua all use &lt;code&gt;dlopen&lt;/code&gt; to pull in native extensions. The interpreter is the host; the extension is the plugin; &lt;code&gt;dlsym&lt;/code&gt; finds the entry point by a known name convention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GPU and audio drivers&lt;/strong&gt; — the OS loads the right driver for the installed hardware without recompiling anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  the four functions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// &amp;lt;dlfcn.h&amp;gt;&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;dlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;dlerror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;   &lt;span class="nf"&gt;dlclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dlopen&lt;/code&gt; maps the library into the process's address space and returns an opaque handle. Two flag pairs matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RTLD_LAZY&lt;/code&gt; — resolve symbols only when called for the first time. Faster startup; missing symbols fail at call time, mid-execution.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RTLD_NOW&lt;/code&gt; — resolve everything immediately. A missing symbol aborts at &lt;code&gt;dlopen&lt;/code&gt; time, not later.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RTLD_LOCAL&lt;/code&gt; (default) — keeps the library's symbols private to this handle.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;RTLD_GLOBAL&lt;/code&gt; — dumps all exported symbols into the process-wide namespace. Every library loaded afterward can see them. PHP uses this; the cost is that two extensions with the same symbol name silently shadow one another based on load order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;dlsym&lt;/code&gt; does a hash lookup for a named symbol and returns its address as &lt;code&gt;void *&lt;/code&gt;. The correct error-checking pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;dlerror&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                           &lt;span class="c1"&gt;// clear any stale error&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sym&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dlerror&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* sym is unusable, do not call it */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking &lt;code&gt;sym != NULL&lt;/code&gt; is not enough — a valid symbol can theoretically reside at address zero, and a failed lookup can return &lt;code&gt;NULL&lt;/code&gt; without setting an error in some edge cases. The &lt;code&gt;dlerror&lt;/code&gt; round-trip is the only reliable path.&lt;/p&gt;

&lt;h2&gt;
  
  
  minimal C23 example
&lt;/h2&gt;

&lt;p&gt;Two translation units: the plugin compiled to &lt;code&gt;.so&lt;/code&gt;, and the host that loads it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// plugin.c — cc -std=c23 -fPIC -fvisibility=hidden -shared -o plugin.so plugin.c&lt;/span&gt;

&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello, %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-fPIC&lt;/code&gt; (position-independent code) is required for shared libraries. Addresses inside the &lt;code&gt;.so&lt;/code&gt; are relative offsets rather than absolute, so the same file maps at different virtual addresses in different processes. &lt;code&gt;-fvisibility=hidden&lt;/code&gt; makes hidden the default; &lt;code&gt;visibility("default")&lt;/code&gt; opts individual symbols back in. Without it, your entire symbol table is exported — a maintenance hazard and a collision risk in large plugin ecosystems.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// host.c — cc -std=c23 -o host host.c -ldl&lt;/span&gt;

&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;dlfcn.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;greet_fn&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;auto&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./plugin.so"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RTLD_NOW&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;RTLD_LOCAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dlopen: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dlerror&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EXIT_FAILURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;dlerror&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;greet_fn&lt;/span&gt; &lt;span class="n"&gt;greet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;greet_fn&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;uintptr_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"greet"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dlerror&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dlsym: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;dlclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EXIT_FAILURE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;dlclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EXIT_SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;auto handle&lt;/code&gt; is valid C23 — the type is inferred as &lt;code&gt;void *&lt;/code&gt; from &lt;code&gt;dlopen&lt;/code&gt;'s return type. Same semantics as C++ &lt;code&gt;auto&lt;/code&gt;, standardized in C 13 years later.&lt;/p&gt;

&lt;p&gt;The cast &lt;code&gt;(greet_fn)(uintptr_t)dlsym(...)&lt;/code&gt; is the standard-conforming path from &lt;code&gt;void *&lt;/code&gt; to a function pointer. ISO C does not guarantee &lt;code&gt;void *&lt;/code&gt; and function pointers share the same representation. POSIX guarantees it specifically for &lt;code&gt;dlsym&lt;/code&gt;, but the double-cast suppresses the pedantic diagnostic cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  how PHP loads extensions
&lt;/h2&gt;

&lt;p&gt;PHP is one specific application of this exact pattern. Its loader lives in &lt;code&gt;main/dl.c&lt;/code&gt;. The core of &lt;code&gt;php_load_extension()&lt;/code&gt; — simplified but structurally faithful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main/dl.c (php-src)&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DL_LOAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;libpath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// DL_LOAD is dlopen() on POSIX&lt;/span&gt;

&lt;span class="n"&gt;zend_module_entry&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;get_module&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"get_module"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;zend_module_entry&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;module_entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_module&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;zend_register_module_ex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module_entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;zend_startup_module_ex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module_entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;get_module&lt;/code&gt; is PHP's &lt;code&gt;update&lt;/code&gt; — a known-name entry point the host finds via &lt;code&gt;dlsym&lt;/code&gt;. The difference is the payload: instead of a function pointer to game logic, it returns a pointer to &lt;code&gt;zend_module_entry&lt;/code&gt;, PHP's struct describing everything the extension provides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;zend_module_entry&lt;/span&gt; &lt;span class="n"&gt;myext_module_entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;STANDARD_MODULE_HEADER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"myext"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;myext_functions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="cm"&gt;/* NULL-terminated Zend function table */&lt;/span&gt;
    &lt;span class="n"&gt;PHP_MINIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="cm"&gt;/* called once at process startup */&lt;/span&gt;
    &lt;span class="n"&gt;PHP_MSHUTDOWN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;PHP_RINIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;     &lt;span class="cm"&gt;/* called per request */&lt;/span&gt;
    &lt;span class="n"&gt;PHP_RSHUTDOWN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;PHP_MINFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;STANDARD_MODULE_PROPERTIES&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/* expands to: zend_module_entry *get_module(void) { return &amp;amp;myext_module_entry; } */&lt;/span&gt;
&lt;span class="n"&gt;ZEND_GET_MODULE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;myext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;RINIT&lt;/code&gt;/&lt;code&gt;RSHUTDOWN&lt;/code&gt; run once per request; &lt;code&gt;MINIT&lt;/code&gt;/&lt;code&gt;MSHUTDOWN&lt;/code&gt; once per worker process lifetime. PHP passes &lt;code&gt;RTLD_GLOBAL | RTLD_LAZY&lt;/code&gt; by default. &lt;code&gt;RTLD_GLOBAL&lt;/code&gt; is intentional — some extensions wrap C++ libraries that need their own symbols visible to later-loaded libraries. The consequence is that two extensions exporting the same symbol name silently shadow each other by load order. No warning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rust without headaches
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;libloading&lt;/code&gt; wraps &lt;code&gt;dlopen&lt;/code&gt;/&lt;code&gt;dlsym&lt;/code&gt; with a type-safe API. The unsafe surface shrinks to the unavoidable: loading the library and declaring the expected function signature.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;libloading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.8"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.rs&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;libloading&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// SAFETY: plugin.so is a well-formed shared library we control.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./plugin.so"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// SAFETY: "greet" exists, takes *const c_char, returns c_int.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Symbol&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;i8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
        &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"greet&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="nf"&gt;.as_ptr&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Library::new&lt;/code&gt; calls &lt;code&gt;dlopen&lt;/code&gt; and maps the error to Rust's &lt;code&gt;Error&lt;/code&gt; trait. &lt;code&gt;lib.get&lt;/code&gt; calls &lt;code&gt;dlsym&lt;/code&gt; — &lt;code&gt;b"greet\0"&lt;/code&gt; is the null-terminated symbol name exactly as &lt;code&gt;dlsym&lt;/code&gt; expects. &lt;code&gt;Symbol&amp;lt;F&amp;gt;&lt;/code&gt; carries the function signature, so calling &lt;code&gt;greet(...)&lt;/code&gt; type-checks the arguments at compile time.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Symbol&amp;lt;T&amp;gt;&lt;/code&gt; borrows from &lt;code&gt;Library&lt;/code&gt;, so the borrow checker prevents calling a symbol after the library is dropped — a use-after-free class of bug that C gives you silently.&lt;/p&gt;

&lt;p&gt;For anything real, wrap the FFI behind a typed struct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_lib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Plugin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;libloading&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;Library&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="c1"&gt;// Dereference copies the fn pointer out of Symbol — fn pointers are Copy,&lt;/span&gt;
        &lt;span class="c1"&gt;// so we own the value before Symbol's borrow of lib ends.&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="py"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"update&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_lib&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;unsafe&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_lib: Library&lt;/code&gt; keeps the shared library alive for the struct's lifetime. Dropping &lt;code&gt;Plugin&lt;/code&gt; calls &lt;code&gt;dlclose&lt;/code&gt;. Callers never see &lt;code&gt;unsafe&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  the ABI in detail
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;dlsym&lt;/code&gt; returns an address and trusts you've declared the right type to call through. There are three independent ways to get that wrong: symbol name, calling convention, and struct layout. Each fails silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  calling convention
&lt;/h3&gt;

&lt;p&gt;A calling convention is the machine-level agreement between caller and callee: which registers carry arguments, who restores the stack, and where the return value lands.&lt;/p&gt;

&lt;p&gt;The x86-64 System V ABI (Linux, macOS, BSDs) assigns integer and pointer arguments in this order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;position&lt;/th&gt;
&lt;th&gt;register&lt;/th&gt;
&lt;th&gt;32-bit alias&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rdi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;edi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2nd&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rsi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;esi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3rd&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rdx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;edx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4th&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rcx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ecx&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5th&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r8d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6th&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r9d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7th+&lt;/td&gt;
&lt;td&gt;stack&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Return value comes back in &lt;code&gt;rax&lt;/code&gt;. Float arguments use &lt;code&gt;xmm0&lt;/code&gt;–&lt;code&gt;xmm7&lt;/code&gt;. Registers &lt;code&gt;rax&lt;/code&gt;, &lt;code&gt;rcx&lt;/code&gt;, &lt;code&gt;rdx&lt;/code&gt;, &lt;code&gt;rsi&lt;/code&gt;, &lt;code&gt;rdi&lt;/code&gt;, &lt;code&gt;r8&lt;/code&gt;–&lt;code&gt;r11&lt;/code&gt; are caller-saved — the callee may clobber them. &lt;code&gt;rbx&lt;/code&gt;, &lt;code&gt;rbp&lt;/code&gt;, &lt;code&gt;r12&lt;/code&gt;–&lt;code&gt;r15&lt;/code&gt; are callee-saved — the callee must restore them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// update(&amp;amp;state, 0.016f) on x86-64 System V:&lt;/span&gt;
&lt;span class="c1"&gt;// rdi = &amp;amp;state   ← pointer to State struct&lt;/span&gt;
&lt;span class="c1"&gt;// xmm0 = 0.016f  ← first float argument&lt;/span&gt;
&lt;span class="c1"&gt;// call &amp;lt;address from dlsym&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Windows x64 differs: &lt;code&gt;rcx&lt;/code&gt;, &lt;code&gt;rdx&lt;/code&gt;, &lt;code&gt;r8&lt;/code&gt;, &lt;code&gt;r9&lt;/code&gt; for the first four, then stack, with 32 bytes of shadow space reserved regardless. A function compiled for one convention, called through the other, reads arguments from wrong registers. No error at any stage — just wrong values.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;extern "C"&lt;/code&gt; in C++ and Rust selects the platform C calling convention. Without it the compiler chooses whatever it wants, and there is no guarantee two compilers choose the same thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  struct layout
&lt;/h3&gt;

&lt;p&gt;C lays out fields in declaration order, each aligned to its own natural size: &lt;code&gt;char&lt;/code&gt; to 1, &lt;code&gt;int&lt;/code&gt; to 4, &lt;code&gt;long&lt;/code&gt; and pointers to 8 on LP64. The struct is padded at the end to a multiple of its largest member's alignment. Gaps are inserted between fields to satisfy alignment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Example&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// offset 0,  size 1&lt;/span&gt;
              &lt;span class="c1"&gt;// ← 3 bytes padding&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;  &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// offset 4,  size 4&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// offset 8,  size 1&lt;/span&gt;
              &lt;span class="c1"&gt;// ← 7 bytes padding&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// offset 16, size 8&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// sizeof == 24, not 14&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;field&lt;/th&gt;
&lt;th&gt;offset&lt;/th&gt;
&lt;th&gt;size&lt;/th&gt;
&lt;th&gt;padding after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reordering to &lt;code&gt;{ char a; char c; int b; long d; }&lt;/code&gt; produces a 16-byte struct. Same fields, same types, different size. A plugin compiled against the 24-byte layout that reads &lt;code&gt;d&lt;/code&gt; at offset 16 reads from the middle of &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt; in the 16-byte layout. No error at any point in the toolchain.&lt;/p&gt;

&lt;p&gt;The standard mitigation: put a &lt;code&gt;version&lt;/code&gt; or &lt;code&gt;size&lt;/code&gt; field first in any struct that crosses the plugin boundary, and check it at load time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PluginAPI&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// must be first, must never move&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// At load time:&lt;/span&gt;
&lt;span class="n"&gt;PluginAPI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_api&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;PLUGIN_API_VERSION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* reject */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  name mangling
&lt;/h3&gt;

&lt;p&gt;C symbol names are function names, verbatim. &lt;code&gt;dlsym(handle, "greet")&lt;/code&gt; finds &lt;code&gt;greet&lt;/code&gt;. C++ encodes namespace, class, and parameter types into the symbol to support overloading. Rust does the same for generics.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# C: int greet(const char *name)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;nm &lt;span class="nt"&gt;-D&lt;/span&gt; plugin_c.so | &lt;span class="nb"&gt;grep &lt;/span&gt;greet
00000000000010f0 T greet

&lt;span class="c"&gt;# C++: int greet(const char *name)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;nm &lt;span class="nt"&gt;-D&lt;/span&gt; plugin_cpp.so | &lt;span class="nb"&gt;grep &lt;/span&gt;greet
00000000000010f0 T _Z5greetPKc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_Z5greetPKc&lt;/code&gt; means: function named &lt;code&gt;greet&lt;/code&gt; (5 chars), taking &lt;code&gt;PKc&lt;/code&gt; (pointer-to-const-char). Decode it with &lt;code&gt;c++filt _Z5greetPKc&lt;/code&gt;. The scheme is not standardized — it differs between GCC and Clang, between versions, between platforms. &lt;code&gt;dlsym(handle, "_Z5greetPKc")&lt;/code&gt; works today and breaks silently after a compiler upgrade.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;extern "C"&lt;/code&gt; suppresses C++ mangling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mangled — unstable symbol name&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Not mangled — stable, dlsym-able by plain name&lt;/span&gt;
&lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rust needs both &lt;code&gt;extern "C"&lt;/code&gt; (for calling convention) and &lt;code&gt;#[no_mangle]&lt;/code&gt; (to emit the plain name):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Mangled + Rust ABI — dlsym("greet") won't find it&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;i8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// C ABI, plain symbol name — dlsym("greet") finds it&lt;/span&gt;
&lt;span class="nd"&gt;#[no_mangle]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;i8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  struct layout in Rust
&lt;/h3&gt;

&lt;p&gt;Without &lt;code&gt;#[repr(C)]&lt;/code&gt;, Rust may reorder struct fields to minimize padding. The layout is unspecified and can change between compiler versions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// repr(Rust) — layout unspecified, compiler may reorder&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Foo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Possible layout: b(0), a(4), c(5), pad(6-7) → 8 bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// repr(C) — C rules, declaration order preserved&lt;/span&gt;
&lt;span class="nd"&gt;#[repr(C)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Foo&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u8&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Guaranteed: a(0), pad(1-3), b(4), c(8), pad(9-11) → 12 bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any struct passed by pointer to C, returned from C, or embedded in a C struct must be &lt;code&gt;#[repr(C)]&lt;/code&gt;. Without it, C reads fields from wrong offsets.&lt;/p&gt;

&lt;h2&gt;
  
  
  where it goes wrong
&lt;/h2&gt;

&lt;p&gt;Symbol versioning exists in glibc — &lt;code&gt;.symver&lt;/code&gt; directives, &lt;code&gt;SONAME&lt;/code&gt; in the ELF header — but almost nothing outside of libc and GPU vendors uses it correctly. If your plugin ABI changes, you change the symbol name or the filename. There is no automatic enforcement.&lt;/p&gt;

&lt;p&gt;Crashes inside a loaded plugin take down the whole process. PHP isolates this with shared-nothing worker processes — one per request — not with any sandbox inside the loader. If you need fault isolation, you need process boundaries or a WASM sandbox, not &lt;code&gt;dlopen&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it isn't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not a substitute for static linking&lt;/strong&gt; when you control both sides and ship them together. Static linking gives the linker visibility to strip dead code, inline across translation units, and catch missing symbols at build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not give you ABI stability for free.&lt;/strong&gt; Reordering struct fields, adding a parameter, or switching compiler versions can break a loaded plugin with zero compile-time signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not safe to pass Rust-native types across the boundary.&lt;/strong&gt; &lt;code&gt;Vec&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;Arc&amp;lt;T&amp;gt;&lt;/code&gt; all depend on allocator identity and internal layout that is not stable across compiler versions. Use &lt;code&gt;*const T&lt;/code&gt;/&lt;code&gt;*mut T&lt;/code&gt; with &lt;code&gt;#[repr(C)]&lt;/code&gt; structs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RTLD_GLOBAL&lt;/code&gt; is not a default you want.&lt;/strong&gt; It pollutes the process-wide symbol namespace. Use &lt;code&gt;RTLD_LOCAL&lt;/code&gt; unless you have a concrete reason, and document it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not work on Windows as written.&lt;/strong&gt; The Windows equivalent is &lt;code&gt;LoadLibrary&lt;/code&gt;/&lt;code&gt;GetProcAddress&lt;/code&gt;. &lt;code&gt;libloading&lt;/code&gt; abstracts over both; the raw POSIX API does not exist on Windows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  where to start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Plugin&lt;/span&gt;
cc &lt;span class="nt"&gt;-std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c23 &lt;span class="nt"&gt;-fPIC&lt;/span&gt; &lt;span class="nt"&gt;-fvisibility&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;hidden &lt;span class="nt"&gt;-shared&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; plugin.so plugin.c

&lt;span class="c"&gt;# Host (Linux requires -ldl; macOS includes it in libSystem automatically)&lt;/span&gt;
cc &lt;span class="nt"&gt;-std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c23 &lt;span class="nt"&gt;-o&lt;/span&gt; host host.c &lt;span class="nt"&gt;-ldl&lt;/span&gt;

./host
&lt;span class="c"&gt;# hello, world&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the hot-reload loop: compile the above, run the host, then edit &lt;code&gt;logic.c&lt;/code&gt;, run &lt;code&gt;make&lt;/code&gt;, and watch the swap happen without restarting.&lt;/p&gt;

&lt;p&gt;For Rust: &lt;code&gt;cargo add libloading&lt;/code&gt;, use the typed wrapper pattern above, keep &lt;code&gt;unsafe&lt;/code&gt; blocks minimal and commented.&lt;/p&gt;

&lt;p&gt;I built the ScyllaDB PHP driver on this foundation — every &lt;code&gt;ZEND_GET_MODULE&lt;/code&gt; is the structured version of what's described here.&lt;/p&gt;

</description>
      <category>c23</category>
      <category>libdl</category>
      <category>dlopen</category>
      <category>dynamiclibrary</category>
    </item>
    <item>
      <title>laravel-crypto: libsodium, no compromises, and per-user encryption</title>
      <dc:creator>Dusan Malusev</dc:creator>
      <pubDate>Mon, 18 May 2026 21:22:54 +0000</pubDate>
      <link>https://forem.com/malusev998/laravel-crypto-libsodium-no-compromises-and-per-user-encryption-3055</link>
      <guid>https://forem.com/malusev998/laravel-crypto-libsodium-no-compromises-and-per-user-encryption-3055</guid>
      <description>&lt;p&gt;Laravel's default cipher is &lt;code&gt;AES-256-CBC&lt;/code&gt;. That is a 25-year-old design with no built-in authentication — the MAC is bolted on separately by the framework, and the correctness of that construction depends on nobody ever reordering the operations.&lt;/p&gt;

&lt;p&gt;I am not saying it is broken. I am saying PHP has shipped with libsodium since 7.2, and libsodium gives you authenticated encryption by construction. There was no reason to keep using &lt;code&gt;AES-256-CBC&lt;/code&gt; as the default the moment &lt;code&gt;sodium_crypto_aead_xchacha20poly1305_ietf_encrypt&lt;/code&gt; became available in every standard PHP install. I built &lt;a href="https://github.com/MalusevDevelopment/laravel-crypto" rel="noopener noreferrer"&gt;laravel-crypto&lt;/a&gt; because I wanted to stop thinking about that gap every time I started a new project.&lt;/p&gt;

&lt;h2&gt;
  
  
  why libsodium
&lt;/h2&gt;

&lt;p&gt;NaCl — and libsodium as its portable successor — was designed around one principle: remove the ways you can shoot yourself in the foot. Every algorithm choice is made for you. There is no ECB mode. There is no "pick your own MAC." XChaCha20-Poly1305 is authenticated by definition. AEGIS was designed specifically for high-throughput AEAD on hardware with AES acceleration, which includes every modern x86 and ARM chip. The API surface is small enough to understand fully.&lt;/p&gt;

&lt;p&gt;PHP's mcrypt was the old answer. It was unmaintained, it supported ECB, it let you combine incompatible primitives, and it was removed in PHP 7.2. libsodium replaced it — but Laravel did not update its default cipher. &lt;code&gt;AES-256-CBC&lt;/code&gt; stayed in &lt;code&gt;config/app.php&lt;/code&gt;, a leftover from before the ecosystem had anything better. laravel-crypto picks up where mcrypt's removal should have taken things.&lt;/p&gt;

&lt;h2&gt;
  
  
  the drop-in
&lt;/h2&gt;

&lt;p&gt;Swapping the provider is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bootstrap/providers.php (Laravel 11+)&lt;/span&gt;
&lt;span class="c1"&gt;// Illuminate\Encryption\EncryptionServiceProvider::class,   // remove&lt;/span&gt;
&lt;span class="nc"&gt;CodeLieutenant\LaravelCrypto\ServiceProvider&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// add&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, &lt;code&gt;Crypt::encryptString()&lt;/code&gt; and &lt;code&gt;Crypt::decryptString()&lt;/code&gt; still work exactly as before. All existing code calling the facade keeps working. The only change is what runs underneath.&lt;/p&gt;

&lt;p&gt;Set the cipher in &lt;code&gt;config/app.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s1"&gt;'cipher'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Sodium_AEGIS256GCM'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="c1"&gt;// Options: Sodium_AES256GCM, Sodium_XChaCha20Poly1305, Sodium_AEGIS256GCM, Sodium_AEGIS128LGCM, Sodium_SecretBox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate keys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan crypto:keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the full migration for most apps. If you already use &lt;code&gt;AEAD&lt;/code&gt; via &lt;code&gt;APP_PREVIOUS_KEYS&lt;/code&gt;, the library picks those up automatically for decryption during key rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  the numbers
&lt;/h2&gt;

&lt;p&gt;Benchmarks from PHP 8.5.1 on a MacBook M4 Pro — Apple Silicon has hardware AES-NI and dedicated AEGIS acceleration:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Algorithm&lt;/th&gt;
&lt;th&gt;1 KiB enc&lt;/th&gt;
&lt;th&gt;1 KiB dec&lt;/th&gt;
&lt;th&gt;1 MiB enc&lt;/th&gt;
&lt;th&gt;1 MiB dec&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Laravel AES-256-CBC&lt;/td&gt;
&lt;td&gt;8.09 μs&lt;/td&gt;
&lt;td&gt;9.98 μs&lt;/td&gt;
&lt;td&gt;5.02 ms&lt;/td&gt;
&lt;td&gt;7.57 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laravel AES-256-GCM&lt;/td&gt;
&lt;td&gt;3.37 μs&lt;/td&gt;
&lt;td&gt;5.33 μs&lt;/td&gt;
&lt;td&gt;1.31 ms&lt;/td&gt;
&lt;td&gt;3.94 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sodium AES-256-GCM&lt;/td&gt;
&lt;td&gt;2.39 μs&lt;/td&gt;
&lt;td&gt;2.58 μs&lt;/td&gt;
&lt;td&gt;1.11 ms&lt;/td&gt;
&lt;td&gt;1.88 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sodium XChaCha20-Poly1305&lt;/td&gt;
&lt;td&gt;3.41 μs&lt;/td&gt;
&lt;td&gt;3.58 μs&lt;/td&gt;
&lt;td&gt;2.21 ms&lt;/td&gt;
&lt;td&gt;2.90 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sodium AEGIS-256&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2.06 μs&lt;/td&gt;
&lt;td&gt;2.27 μs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.82 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.65 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sodium AEGIS-128L&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.03 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.14 μs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.90 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.60 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;AEGIS-128L decrypts a 1 MiB payload in 1.60 ms versus Laravel's AES-256-CBC at 7.57 ms. That is 4.7× faster, authenticated, on the same hardware. Sodium AES-256-GCM halves the decryption time compared to Laravel's own GCM implementation — same algorithm, better path through the Sodium extension than through OpenSSL via PHP's stream wrapper.&lt;/p&gt;

&lt;p&gt;XChaCha20-Poly1305 is the conservative choice: well-analyzed, hardware-agnostic, fast on everything. Use it if you need consistent performance on older or constrained hardware without AES acceleration. On ARM or x86 with AES-NI, AEGIS is the right pick.&lt;/p&gt;

&lt;p&gt;The point is not that the difference matters for a single request. It matters when you are decrypting a hundred fields per page load, or processing a batch job over encrypted rows, or streaming large files. &lt;code&gt;AES-256-CBC&lt;/code&gt; at 7.57 ms per MiB is not a performance problem in isolation. As a default that never gets questioned, it adds up.&lt;/p&gt;

&lt;h2&gt;
  
  
  per-user encryption
&lt;/h2&gt;

&lt;p&gt;The standard model — one &lt;code&gt;APP_KEY&lt;/code&gt; encrypts everything — has a structural problem. Anyone with access to that key can decrypt every row in the database. That is the DBA, the sysadmin, the developer with a &lt;code&gt;.env&lt;/code&gt; copy, and any process with access to the environment. If client data confidentiality is a real requirement, "we encrypted the database" is not a complete answer when a single key unlocks all of it.&lt;/p&gt;

&lt;p&gt;Per-user encryption means each user's sensitive fields are encrypted with a key unique to that user. A 32-byte random key is generated at enrollment and wrapped in a self-contained blob stored in the &lt;code&gt;encryption_key&lt;/code&gt; column. Two wrapping modes exist depending on what is available at enrollment time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mode 1 — password-wrapped (0x01, 89 bytes):&lt;/strong&gt; wrapping key derived via Argon2id from the user's plaintext password. &lt;code&gt;APP_KEY&lt;/code&gt; is not involved. Unwrapping requires the password — no server-side secret alone can decrypt these fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mode 2 — server-wrapped (0x02, 73 bytes):&lt;/strong&gt; wrapping key derived via &lt;code&gt;BLAKE2b(appKey, userId)&lt;/code&gt;. Used for auto-enrollment when the plaintext password is unavailable (e.g. OAuth users, Filament). The blob is promoted to Mode 1 transparently the next time the user provides their password.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The unwrapped key is never stored anywhere. It is base64url-encoded, sent to the client as &lt;code&gt;X-Encryption-Token&lt;/code&gt; (header for SPA/API clients) or as an HTTP-only &lt;code&gt;enc_token&lt;/code&gt; cookie (web clients), and re-sent on every subsequent request. The middleware reads it, loads it into the request-scoped context, then zeros it at the end of the response.&lt;/p&gt;

&lt;p&gt;Setting it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// User model&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;CodeLieutenant\LaravelCrypto\Traits\HasUserEncryption&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Authenticatable&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;HasUserEncryption&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At registration, return the token to the client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$rawKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;initUserEncryption&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Encryption-Token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;encodeEncryptionToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rawKey&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At login, re-derive the token from the password and hand it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$credentials&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Auth&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;issueEncryptionToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$credentials&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'password'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'X-Encryption-Token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, model casts handle the rest transparently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserSecret&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;casts&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'ssn'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;UserEncryptedWithIndex&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;':ssn_index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading &lt;code&gt;$secret-&amp;gt;ssn&lt;/code&gt; decrypts using the key currently in the request context. Saving writes ciphertext. Nothing else in the application changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  the key in memory
&lt;/h2&gt;

&lt;p&gt;The key lives in a &lt;code&gt;scoped&lt;/code&gt; container — one instance per HTTP request. The &lt;code&gt;BootPerUserEncryption&lt;/code&gt; middleware loads it from the incoming header or cookie, sets it on the context, then clears it in a &lt;code&gt;finally&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Encryption/UserKey/UserEncryptionContext.php&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;sodium_memzero&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__destruct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setting a variable to &lt;code&gt;null&lt;/code&gt; in PHP does not zero the memory — the garbage collector might hold the reference, and even when it collects, it does not overwrite the bytes. &lt;code&gt;sodium_memzero&lt;/code&gt; does. The key is gone, not just unreferenced.&lt;/p&gt;

&lt;p&gt;For Mode 1 blobs the wrapped blob in the database is useless without the user's password. Compromising &lt;code&gt;APP_KEY&lt;/code&gt; does not help. For Mode 2 blobs, &lt;code&gt;APP_KEY&lt;/code&gt; + &lt;code&gt;userId&lt;/code&gt; is enough to re-derive the wrapping key — that is the acknowledged trade-off for the auto-enrollment path. The moment the user sets a password, &lt;code&gt;issueEncryptionToken()&lt;/code&gt; promotes the blob to Mode 1 automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  blind indexes for searchable fields
&lt;/h2&gt;

&lt;p&gt;Encrypting a column means you can no longer query it. &lt;code&gt;WHERE ssn = ?&lt;/code&gt; stops working when &lt;code&gt;ssn&lt;/code&gt; is ciphertext. The standard solution is a blind index: a deterministic MAC of the plaintext stored alongside the ciphertext, used only for equality lookups.&lt;/p&gt;

&lt;p&gt;The derivation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Encryption/UserKey/BlindIndex.php — compute&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;SensitiveParameter&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$normalise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$userKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$subKey&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;deriveColumnSubKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$normalise&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;mb_strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sodium_crypto_generichash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$subKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;INDEX_BYTES&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// src/Encryption/UserKey/BlindIndex.php — column sub-key&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;deriveColumnSubKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$userKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sodium_crypto_generichash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SODIUM_CRYPTO_GENERICHASH_BYTES_MIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$ctx&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sodium_crypto_kdf_derive_from_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;INDEX_BYTES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;KDF_SUBKEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$userKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each column gets a distinct sub-key derived from the user's key via libsodium's official KDF. The index for &lt;code&gt;ssn&lt;/code&gt; is cryptographically separated from the index for &lt;code&gt;email&lt;/code&gt;, even though both come from the same user key. Two users with identical SSNs produce different indexes. Querying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;UserSecret&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ssn_index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserCrypt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;blindIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'123-45-6789'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'ssn'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leakage is explicit: blind indexes leak equality. An attacker with database read access can tell that two rows share a value — they learn nothing about what that value is. Do not put a blind index on a low-cardinality field. &lt;code&gt;gender&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;, &lt;code&gt;boolean&lt;/code&gt; flags — no. SSN, passport number, phone, email — yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it isn't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is not a replacement for proper key management. Mode 1 blobs are only as safe as the user's password — weak passwords mean weak wrapping. Mode 2 blobs are only as safe as &lt;code&gt;APP_KEY&lt;/code&gt;. Treat &lt;code&gt;APP_KEY&lt;/code&gt; accordingly: secrets manager, rotation policy, backup.&lt;/li&gt;
&lt;li&gt;It does not protect against runtime compromise. If an attacker can execute arbitrary PHP in your process, they can read the key from the request context during a live request. Encryption protects data at rest, not running code.&lt;/li&gt;
&lt;li&gt;Per-user encryption is not transparent key rotation. Changing a user's password requires &lt;code&gt;rewrapUserEncryption($old, $new)&lt;/code&gt;. Rotating &lt;code&gt;APP_KEY&lt;/code&gt; requires re-wrapping every user's key. Neither is automatic.&lt;/li&gt;
&lt;li&gt;Blind indexes leak equality. Two rows with the same value in the same column for the same user produce the same index. On fields with a small set of possible values, that is dangerous enough to skip the index entirely.&lt;/li&gt;
&lt;li&gt;It is not end-to-end encryption. The raw key is derived server-side and then handed to the client as a token. The server sees it. True E2E requires the key to never reach the server — that is a different architecture and a different threat model entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  where to start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require codelieutenant/laravel-crypto
php artisan vendor:publish &lt;span class="nt"&gt;--provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"CodeLieutenant&lt;/span&gt;&lt;span class="se"&gt;\L&lt;/span&gt;&lt;span class="s2"&gt;aravelCrypto&lt;/span&gt;&lt;span class="se"&gt;\S&lt;/span&gt;&lt;span class="s2"&gt;erviceProvider"&lt;/span&gt;
php artisan crypto:keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;'cipher' =&amp;gt; 'Sodium_AEGIS256GCM'&lt;/code&gt; in &lt;code&gt;config/app.php&lt;/code&gt;, swap the service provider, run your test suite. For most apps the migration takes twenty minutes.&lt;/p&gt;

&lt;p&gt;Per-user encryption takes longer — you need to decide which columns are sensitive enough to warrant it, wire up the middleware, and handle the password-change re-wrap flow. The docs in &lt;code&gt;docs/UserEncryption.md&lt;/code&gt; cover the full setup. I use it in every project where the question "can your team read this user's data?" has an answer I would be uncomfortable defending.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>encryption</category>
      <category>libsodium</category>
    </item>
    <item>
      <title>ScyllaDB PHP Driver: the story so far</title>
      <dc:creator>Dusan Malusev</dc:creator>
      <pubDate>Mon, 18 May 2026 21:22:28 +0000</pubDate>
      <link>https://forem.com/malusev998/scylladb-php-driver-the-story-so-far-3e9j</link>
      <guid>https://forem.com/malusev998/scylladb-php-driver-the-story-so-far-3e9j</guid>
      <description>&lt;p&gt;The DataStax PHP driver for Cassandra would not compile on PHP 8. That was the whole problem.&lt;/p&gt;

&lt;p&gt;At the time I was working at Nano Interactive. The entire backend was PHP. We were running Apache Cassandra as a primary data store — session data, event pipelines, a few hot lookup tables — and the driver that talked to it was the official DataStax extension. It worked fine on PHP 7. Then PHP 8 came out, we needed to upgrade, and the extension refused to build. Compile errors in the Zend internals layer. &lt;code&gt;make&lt;/code&gt; spitting out pages of incompatible API warnings.&lt;/p&gt;

&lt;p&gt;I went looking for a fix. There was none. DataStax had effectively abandoned the project — no PHP 8 tag, no branch, no response on the open issues. The last meaningful release was years old. The options were: stay on PHP 7 indefinitely, rewrite our Cassandra layer to use something else, or fix the driver ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  why not just switch
&lt;/h2&gt;

&lt;p&gt;Switching looked easy on paper. In practice the codebase had Cassandra-specific types everywhere — &lt;code&gt;Cassandra\Uuid&lt;/code&gt;, &lt;code&gt;Cassandra\Timestamp&lt;/code&gt;, &lt;code&gt;Cassandra\Bigint&lt;/code&gt; — not because of architectural brilliance but because the extension had its own type system and those types had leaked into application code over years. Replacing the driver meant touching hundreds of files. And we had no guarantee that a different client library would give us the same behavioral semantics — retry policies, paging state, consistency levels.&lt;/p&gt;

&lt;p&gt;There was also no other option in the PHP ecosystem. No community-maintained pure-PHP client existed that came close in performance — the extension is a thin wrapper over the ScyllaDB C/C++ driver, which means shard-aware routing and native binary protocol handling at C++ speed. A pure-PHP implementation would have been an order of magnitude slower. The realistic alternatives were: stay on PHP 7 until it became a security liability, or abandon PHP entirely and rewrite the service in another language.&lt;/p&gt;

&lt;p&gt;Staying on PHP 7 was not a permanent answer. Security patches. Framework compatibility. PHP 7 EOL was already behind us. Rewriting the service was months of work with no guarantee the result would be better. So I forked the driver and started reading Zend source.&lt;/p&gt;

&lt;h2&gt;
  
  
  learning the zend engine the hard way
&lt;/h2&gt;

&lt;p&gt;PHP extensions are C code that hooks into the Zend Engine through a set of macros and function tables. In principle that is not complicated. In practice the codebase I inherited had been written to support PHP 5, 6, and 7 simultaneously through a layer of compatibility macros with names like &lt;code&gt;PHP5TO7_ZEND_OBJECT&lt;/code&gt;, &lt;code&gt;PHP5TO7_ZEND_HASH_FOREACH_STR_KEY_VAL&lt;/code&gt;, and &lt;code&gt;PHP5TO7_ZVAL_MAYBE_DESTROY&lt;/code&gt;. There were dozens of them, some wrapping trivial one-liners, some hiding real semantic differences between PHP versions.&lt;/p&gt;

&lt;p&gt;The Zend Engine's object model changed significantly between PHP 7 and PHP 8. &lt;code&gt;zend_parse_parameters&lt;/code&gt; deprecated half its format specifiers. &lt;code&gt;get_properties&lt;/code&gt; and &lt;code&gt;get_gc&lt;/code&gt; handlers changed signatures. And the macros — originally written to smooth over the 5→7 transition — did not account for 8 at all.&lt;/p&gt;

&lt;p&gt;I fixed the compile errors. PHP 8.0 worked. Then 8.1. Then 8.2, which introduced new deprecations of its own — &lt;code&gt;__toString&lt;/code&gt; prototype mismatches, &lt;code&gt;zend_hash_sort&lt;/code&gt; API changes. Each minor version was another round of "which macro is lying to me now."&lt;/p&gt;

&lt;p&gt;This was also when I realized the original driver had been written in C, and that C was making the maintenance problem worse. Not because C is wrong for PHP extensions — it is perfectly valid — but because the compatibility macro layer had produced code that was almost impossible to read. You needed three mental translation steps to understand what any given function actually did at runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  the decision i still regret
&lt;/h2&gt;

&lt;p&gt;In March 2023 I renamed every &lt;code&gt;.c&lt;/code&gt; file to &lt;code&gt;.cpp&lt;/code&gt;. All of them. In one pull request.&lt;/p&gt;

&lt;p&gt;The rationale made sense at the time: C++ gives you RAII, references that cannot be null, &lt;code&gt;std::string&lt;/code&gt; instead of manual &lt;code&gt;char *&lt;/code&gt; arithmetic, better type inference. The Zend API is a C API but you can call it from C++ with &lt;code&gt;extern "C"&lt;/code&gt;. ScyllaDB's own C/C++ driver — which we wrap — is C++ already. The pieces would fit together.&lt;/p&gt;

&lt;p&gt;What I underestimated was how much of the existing code relied on C idioms that do not translate cleanly to C++: implicit &lt;code&gt;void *&lt;/code&gt; casts everywhere, VLAs, designated initializers used in ways the C++ standard does not permit. The conversion produced warnings. Some warnings were bugs. Tracking down which was which took months.&lt;/p&gt;

&lt;p&gt;And the toolchain story got harder. Finding people who were comfortable contributing to a PHP extension was already difficult. Finding people comfortable with a PHP extension written in C++ that wraps a C++ driver and uses CMake — that narrowed the pool considerably.&lt;/p&gt;

&lt;p&gt;I would have been better off keeping C and just deleting the compatibility macro layer one file at a time. The abstraction was the problem, not the language.&lt;/p&gt;

&lt;h2&gt;
  
  
  cmake and the build system swamp
&lt;/h2&gt;

&lt;p&gt;The original extension used &lt;code&gt;config.m4&lt;/code&gt; — the PHP build system, based on autoconf. It worked, but it was opaque, hard to extend, and did not give you any way to express modern dependency relationships cleanly. Adding the ScyllaDB C/C++ driver as a bundled dependency was painful.&lt;/p&gt;

&lt;p&gt;I replaced it with CMake. That decision I do not regret — CMake at least has documentation you can actually read. But learning it was its own project. The PHP extension build model assumes phpize generates your Makefile. CMake generates its own build system and has to replicate what phpize does: finding the PHP headers, setting the correct CFLAGS, linking against the right interpreter library, installing the &lt;code&gt;.so&lt;/code&gt; to the right extension directory.&lt;/p&gt;

&lt;p&gt;Getting that working took several iterations. Getting it to work on GitHub Actions — across PHP 8.1, 8.2, 8.3, multiple Linux distributions, both thread-safe and non-thread-safe builds — took longer. The CI was a second project hiding inside the first one. There were commits like &lt;code&gt;fix(release): release fixed&lt;/code&gt; appearing three times in a row because I had been testing the release workflow by pushing tags and watching it fail.&lt;/p&gt;

&lt;p&gt;libuv was its own subplot. The ScyllaDB C++ driver depends on libuv. Building libuv as a static dependency and linking it into a shared &lt;code&gt;.so&lt;/code&gt; requires &lt;code&gt;-fPIC&lt;/code&gt; everywhere. Miss it in one place — the C++ driver's own cmake build, for instance — and you get a linker error that looks completely unrelated to the actual cause. I have that error memorized.&lt;/p&gt;

&lt;h2&gt;
  
  
  daniel and he4rt
&lt;/h2&gt;

&lt;p&gt;At some point in 2023, Daniel — danielhe4rt — joined ScyllaDB and started looking at the PHP driver situation. He found the fork, sent a message, and we started working on it together under the he4rt organization.&lt;/p&gt;

&lt;p&gt;Working with Daniel changed the pace of the project. He had a different focus — documentation, making the thing actually approachable for people who were not deep in the Zend internals — which complemented the low-level work I was doing. The test suite migrated from Behat to Pest. The README became legible. The he4rt organization became the home for the project.&lt;/p&gt;

&lt;p&gt;The driver got PHP 8.3 support in late 2023. PIE packaging in early 2025. macOS and Apple Silicon in 2026. Each of those required fixing something that had been quietly broken for a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  where it is now
&lt;/h2&gt;

&lt;p&gt;The codebase is functional but not clean. There are still hundreds of &lt;code&gt;PHP5TO7_*&lt;/code&gt; macros in the source — the same ones that caused problems in 2022 — because removing them safely requires understanding each one individually and verifying the replacement compiles and behaves correctly across every supported PHP version. We are doing this in staged waves. Wave 1 is done. Wave 2 is in progress.&lt;/p&gt;

&lt;p&gt;The PHP stubs — &lt;code&gt;.stub.php&lt;/code&gt; files that describe the extension's public API and generate the argument info tables that PHP uses for reflection, named arguments, and IDE support — are about 70% complete. The remaining classes have hand-written arginfo that was correct for PHP 7 and may or may not be correct now.&lt;/p&gt;

&lt;p&gt;PHP 8.5 is coming. Some of the GC handler signatures changed again. Those fixes are already in.&lt;/p&gt;

&lt;h2&gt;
  
  
  what i want to do next
&lt;/h2&gt;

&lt;p&gt;AI tooling has made this kind of mechanical refactoring much faster than it used to be. Working through the macro purge with an AI assistant that understands the Zend API has compressed weeks of careful reading into days. I want to spend more structured time on the project this year — not just fixing what's broken when someone files an issue, but actually finishing the cleanup work that has been sitting in the backlog.&lt;/p&gt;

&lt;p&gt;That means completing the stub coverage, removing the remaining PHP5TO7 macros, getting the full test suite green across PHP 8.2 through 8.5, and publishing proper releases on a predictable schedule. The PIE packaging is already there. The GitHub Actions pipeline works. The pieces exist; they need to be connected.&lt;/p&gt;

&lt;p&gt;The driver is useful. People are using it — in Serbia, in Brazil, in teams I have never talked to who found it through Packagist. That matters enough to do the job properly.&lt;/p&gt;

&lt;p&gt;There is also a series of posts I want to write alongside this work. The PHP extension ecosystem has almost no approachable documentation for people starting from scratch in 2025. Most tutorials assume autoconf, PHP 7, and C89. I want to write about building a PHP extension from zero using C23 and CMake — including the bridge between CMake and the &lt;code&gt;config.m4&lt;/code&gt; build system that &lt;code&gt;phpize&lt;/code&gt; expects, which is the part nobody documents clearly. And about publishing it on PIE, the modern PHP extension installer that finally makes distributing compiled extensions sane.&lt;/p&gt;

&lt;p&gt;Beyond C, the official stance is that PHP extensions must be written in C or C++. That is technically true — the Zend API is a C API — but it is not the whole story. Rust can call C APIs. Zig can call C APIs. You can write the glue layer in C and implement the actual logic in whatever compiled language you want. I plan to write about that: what it actually takes to build a PHP extension in Rust, in Zig, in anything that can produce a shared library and link against &lt;code&gt;libphp&lt;/code&gt;. The ecosystem deserves more than one path.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it isn't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is not an official ScyllaDB product — even though I now work at ScyllaDB. It started as a community fork of an abandoned DataStax project and that is still what it is. Nothing here represents ScyllaDB's roadmap or commitments.&lt;/li&gt;
&lt;li&gt;It does not have a test suite that covers every API surface. Large sections of the schema metadata API are untested.&lt;/li&gt;
&lt;li&gt;It does not have a stable ABI across PHP minor versions. You recompile for each PHP version. This is true of all PHP extensions, but worth stating.&lt;/li&gt;
&lt;li&gt;The C-to-C++ migration added real complexity. Contributing to this extension requires understanding both the Zend C API and C++17. That is a higher bar than it should be.&lt;/li&gt;
&lt;li&gt;The macro cleanup is not done. There are correctness bugs hiding in the legacy layer. We find them by running the test suite, and the test suite does not cover everything yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is in a better place than it was in 2022. It is not where I want it to be. I am still working on it.&lt;/p&gt;

</description>
      <category>php</category>
      <category>scylladb</category>
      <category>cassandra</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
