DEV Community

Cover image for How to load an SQLite extension in PDO?
Arnout Boks for Moxio

Posted on • Originally published at moxio.com

How to load an SQLite extension in PDO?

Recently we ran into the need to load an extension for SQLite using PHP's PDO database abstraction. Unfortunately, this is not supported out of the box. Eventually we found a solution involving FFI and Z-Engine. In this blog post I will take you on a tour of our solution and the black magic behind it.

The problem

At Moxio we make extensive use of geospatial database functions. Whether it's for infrastructural construction projects or rule management for the living environment, a lot of objects are bound to a specific geographic location and need to be queried as such. In production we run MariaDB as a database engine, which has geospatial support baked in. For integration tests however we use the lightweight SQLite database engine, which does not come with geospatial functions out of the box. The SpatiaLite extension for SQLite can be used for this.

Currently we use a homemade database abstraction layer, which uses the SQLite3 PHP extension for communicating with SQLite. This PHP extension allows loading SQLite extensions such as SpatiaLite using the SQLite3::loadExtension method. So far all is well.

At the moment we're looking at switching our own database abstraction layer for the Doctrine DBAL. It simply doesn't make sense anymore to maintain our own. We'd rather use a battle-tested open source solution and try to contribute back some of our code if possible, so that everybody benefits.

The problem is that the Doctrine DBAL implementation is not based on PHP's SQLite3 extension, but basically a wrapper around PDO, which is already a database abstraction layer. It therefore contains few SQLite-specific features. There are three sqlite-prefixed functions in PDO, but the possibility to load an SQLite extension is not one of them. This is registered in the PHP issue tracker and there's an implementation PR on GitHub, but that one didn't make it into PHP 7.4. So, no Doctrine DBAL for us?

Exploring solutions

Naturally, we started thinking of solutions. Even if PHP didn't expose the functionality to load extensions, maybe we could still call it directly through the SQLite C API. The Foreign Function Interface (FFI) introduced in PHP 7.4 would be a good candidate to do this. However, if we look at the relevant function declaration in the C API we notice a problem:

int sqlite3_load_extension(
  sqlite3 *db,          /* Load the extension into this database connection */
  const char *zFile,    /* Name of the shared library containing extension */
  const char *zProc,    /* Entry point.  Derived from zFile if 0 */
  char **pzErrMsg       /* Put error message here if not 0 */
);

The sqlite3_load_extension requires a pointer to the database connection we want to load the extension into, which we don't have in PHP. We must find some trick to obtain it from the PDO object.

Luckily, through trips to phpCE 2018 and Bulgaria PHP 2019 I've come to know Alexander Lisachenko and his work on Z-Engine. Z-Engine is a PHP library that uses FFI to hook into the Zend Engine, providing access to PHP's internal structures. Maybe this would allow obtaining the internal sqlite3 pointer behind a PDO object? Indeed, after reaching out Twitter, Alexander confirmed that this would probably be possible using Z-Engine.

Hence I set out to try a solution based on Z-Engine, but soon another challenge presented itself. I had hoped that the PDO object would have some sort of direct reference (something like a private variable) to the internal SQLite connection. This turned out not to be the case. Instead, the PDO implementation uses a common C trick where the PDO object and its internal structures are aligned next to eachother in memory. In the C code they use a function php_pdo_dbh_fetch_inner to shift in memory between the PDO object and the internal struct. We cannot use this function from FFI hoewever, since it is inlined and thus not exposed as a separate function to the outside world. I reached out to Alexander once again, and he and Nikita Popov pointed me to a trick involving handler->offset to obtain the correct memory offset to implement the memory shift myself.

Putting it all together

This proved to be the missing puzzle piece. It still took some time to get used to working with PHP FFI, but eventually I got a working solution. First we need to obtain a C pointer to the relevant PDO object using Z-Engine:

use ZEngine\Core;
use ZEngine\Reflection\ReflectionValue;

Core::init();

$pdo_refl_value = new ReflectionValue($pdo);
$pdo_obj_pointer = $pdo_refl_value->getRawObject();

We then initialize an FFI instance to traverse the relevant structures of the PDO-SQLite extension. To keep the header definitions brief, we omit all parts of C structs after the properties we're interested in. Also, we replace pointers to properties before the ones we want (and the sqlite3 pointer itself for now) by void pointers. This allows us to omit the headers for their real types, and doesn't matter since a pointer takes the same amount of memory regardless of what it points to. Also, we had to add a struct keyword in some places to please the PHP FFI parser.

$pdo_sqlite_ffi = \FFI::cdef(<<<CDEF
/* From https://github.com/php/php-src/blob/d1764ca33018f1f2e4a05926c879c67ad4aa8da5/ext/pdo/php_pdo_driver.h#L432 */
struct _pdo_dbh_t {
    /* replaced pdo_dbh_methods* by void* */
    const void *methods;
    void *driver_data;
    /* omitted rest of struct */
};
/* From https://github.com/php/php-src/blob/d1764ca33018f1f2e4a05926c879c67ad4aa8da5/ext/pdo/php_pdo_driver.h#L510 */
struct _pdo_dbh_object_t {
    /* had to insert struct keyword here */
    struct _pdo_dbh_t *inner;
    /* omitted `zend_object std` */
};
/* Adapted from https://github.com/php/php-src/blob/cfc704ea83c56970a72756f7d4fe464885445b5e/ext/pdo_sqlite/php_pdo_sqlite_int.h#L55 */
struct pdo_sqlite_db_handle {
    /* replaced sqlite3* by void* */
    void *db;
    /* omitted rest of struct */
};
CDEF, "pdo_sqlite.so");

With that FFI instance we can now perform the memory shift to get the PDO internal data structure corresponding to the exposed PDO object. We use the handle->offset trick to obtain the correct memory offset for the shift:

// Following https://github.com/php/php-src/blob/d1764ca33018f1f2e4a05926c879c67ad4aa8da5/ext/pdo/php_pdo_driver.h#L520
$offset = $pdo_obj_pointer->handlers->offset;
$pdo_dbh_object_pointer = $pdo_sqlite_ffi->cast("struct _pdo_dbh_object_t*", $pdo_sqlite_ffi->cast("char*", $pdo_obj_pointer) - $offset);
$pdo_dbh_pointer = $pdo_dbh_object_pointer[0]->inner;

The driver-specific handles are in the driver_data property. In case of the PDO SQLite driver this points to a pdo_sqlite_db_handle object, which contains the raw SQLite connection in its db property. We obtain a void pointer to that connection as follows:

// Following https://github.com/php/php-src/pull/3368/files#diff-eb26679695f7db289366ef6b03ee25daR729
$pdo_sqlite_db_handle_pointer = $pdo_sqlite_ffi->cast("struct pdo_sqlite_db_handle*", $pdo_dbh_pointer[0]->driver_data);
$sqlite3_void_pointer = $pdo_sqlite_db_handle_pointer[0]->db;

Now we set up an FFI object for communicating with the SQLite C API (or at least the parts of it we want to use):

$sqlite3_ffi = \FFI::cdef(<<<CDEF
/* From https://github.com/sqlite/sqlite/blob/278b0517d88d4150830a4ee2c628a55da40d186d/src/sqlite.h.in#L249 */
typedef struct sqlite3 sqlite3;

/* From https://github.com/sqlite/sqlite/blob/278b0517d88d4150830a4ee2c628a55da40d186d/src/sqlite.h.in#L6581 */
int sqlite3_load_extension(
  sqlite3 *db,          /* Load the extension into this database connection */
  const char *zFile,    /* Name of the shared library containing extension */
  const char *zProc,    /* Entry point.  Derived from zFile if 0 */
  char **pzErrMsg       /* Put error message here if not 0 */
);
CDEF, "sqlite3.so");

We can then cast our void pointer to an actual sqlite3 pointer and use it to call functions on the SQLite C API:

$sqlite3_pointer = $sqlite3_ffi->cast("struct sqlite3*", $sqlite3_void_pointer);
$sqlite3_ffi->sqlite3_load_extension($sqlite3_pointer, "mod_spatialite.so", null, null);

And voila, our extension is loaded!

Wrapping up

To make this solution easy to use, we published it as a Composer package. It hides all underlying Z-Engine and FFI logic and allows loading SQLite extensions through a simple API. We aim to grow this library to also provide access to other SQLite API's that are otherwise not available in PHP. If there are other SQLite features that you'd like to use with PHP, feel free to file a feature request or create a PR!

Finally, please note that Z-Engine should not be considered stable before version 1.0.0. Use it at your own risk.

Top comments (0)