HomeRésuméOpen SourceWorkBlog
    Table of contents
  1. (top)
  2. Motivation
  3. Manual Bindings
  4. Bindgen and build.rs
  5. Customizing bindings

RSPHP Part 1: Bindings

Friday, 06 November 2026

Last month I led a workshop at Longhorn PHP about how to build PHP extensions in Rust. The extension that was built during that workshop made use of RSPHP, a set of Rust crates that I wrote to make it easier to develop PHP extensions in Rust.

In this post, I'll talk about the first of these crates, rsphp-sys, that provides FFI bindings for the C code used to implement PHP. Future posts will dive into other parts of the RSPHP code.

References to PHP use the latest stable release as of writing (PHP 8.5.12), likewise with Rust (Rust 1.99.1).

Motivation

When trying to write code in Rust to integrate with an existing C codebase (PHP is written in C), one question that emerges relatively early in the process is how to tell Rust about the types and functions in C that are relevant. For PHP extensions written in C, they can just import the relevant C header files, but that isn't an option in Rust.

Types in Rust can use the #[repr(C)] attribute, which tells the Rust compiler to represent the type in a manner interoperable with C code. External C functions that will be called from Rust are indicated using an "extern" block with function declarations that don't have bodies.

For example, the PHP function highlight_file() internally uses a C function of the same name to do the highlighting. The relevant parts of zend_highlight.h are:

typedef struct _zend_syntax_highlighter_ini {
	char *highlight_html;
	char *highlight_comment;
	char *highlight_default;
	char *highlight_string;
	char *highlight_keyword;
} zend_syntax_highlighter_ini;

// ...

zend_result highlight_file(const char *filename, zend_syntax_highlighter_ini *syntax_highlighter_ini);

To tell Rust about that type and function (zend_result is defined elsewhere), one could write:

#[repr(C)]
pub struct _zend_syntax_highlighter_ini {
    pub highlight_html: *mut c_char,
    pub highlight_comment: *mut c_char,
    pub highlight_default: *mut c_char,
    pub highlight_string: *mut c_char,
    pub highlight_keyword: *mut c_char,
}
pub type zend_syntax_highlighter_ini = _zend_syntax_highlighter_ini;

unsafe extern "C" {
	pub fn highlight_file(
		filename: *const c_char,
		syntax_highlighter_ini: *mut zend_syntax_highlighter_ini,
	) -> zend_result;
}

This Rust code declares

  1. the _zend_syntax_highlighter_ini type, with its five character pointer fields;
  2. the zend_syntax_highlighter_ini alias, corresponding to the C typedef; and
  3. the highlight_file function.

Seems simple enough, right?

Manual Bindings

PHP supports loading extensions dynamically, via configuration using the extension_dir and extension INI directives, or even at runtime with the dl() function depending on configuration. To implement this functionality, PHP uses dynamic linking, via dlsym() when that function is present, or using GetProcAddress() on Windows, to look for the get_module symbol in the compiled extension file. The bare minimum needed to implement a dynamically loadable PHP extension is to define an exported function named get_module that, when called, returns a pointer to a zend_module_entry object.

But, what does that module entry type look like? From zend_modules.h:

#define INIT_FUNC_ARGS		int type, int module_number
#define SHUTDOWN_FUNC_ARGS	int type, int module_number
#define ZEND_MODULE_INFO_FUNC_ARGS zend_module_entry *zend_module

struct _zend_ini_entry;
typedef struct _zend_module_entry zend_module_entry;
typedef struct _zend_module_dep zend_module_dep;

struct _zend_module_entry {
	unsigned short size;
	unsigned int zend_api;
	unsigned char zend_debug;
	unsigned char zts;
	const struct _zend_ini_entry *ini_entry;
	const struct _zend_module_dep *deps;
	const char *name;
	const struct _zend_function_entry *functions;
	zend_result (*module_startup_func)(INIT_FUNC_ARGS);
	zend_result (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS);
	zend_result (*request_startup_func)(INIT_FUNC_ARGS);
	zend_result (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS);
	void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS);
	const char *version;
	size_t globals_size;
#ifdef ZTS
	ts_rsrc_id* globals_id_ptr;
#else
	void* globals_ptr;
#endif
	void (*globals_ctor)(void *global);
	void (*globals_dtor)(void *global);
	zend_result (*post_deactivate_func)(void);
	int module_started;
	unsigned char type;
	void *handle;
	int module_number;
	const char *build_id;
};

At first glance, that isn't too complicated. There are integer fields of various flavors, some null-terminated C strings, and a handful of callback functions for various lifecycle events. Even the pointer to a _zend_module_dep array isn't too challenging to write out bindings for if you examine that type. The _zend_ini_entry pointer is a bit more complicated as it brings in a few other types that need to be declared in Rust, but is still manageable.

The real pain point is the pointer to a _zend_function_entry - check out that type in the generated binding documentation. Once you dive into what is needed to represent a PHP function entry, you quickly run into more and more types that need to be declared to fully represent a function entry, including

and so much more.

I actually did try to write out the bindings for these by hand to start with, using void pointers as a temporary measure to be able to get something to compile, but that wasn't sustainable, especially since these are just the requirements to declare a PHP extension - we haven't even gotten to trying to add any functionality!

Bindgen and build.rs

Enter bindgen, a tool that generates Rust FFI bindings for C code automatically. The links to the different types above have been to the bindings generated with bindgen and documented on docs.rs.

Bindgen was definitely the tool that made it possible for me to write a PHP extension in Rust independently in a reasonable amount of time, and is one of the most widely used tools for creating Rust bindings for existing C projects. But just creating those bindings once isn't enough. Different developers have different machines with different architectures, and PHP itself supports a host of configuration options that control how it will be built. Depending on some of those options, functions may have different parameters and types can even have different fields - recall the #ifdef ZTS in the definition of _zend_module_entry above. The fields of a module entry are different depending on whether PHP was compiled with thread safety enabled.

Bindgen is thus frequently paired with build scripts, mini Rust programs that are run before the actual crate is compiled. That is exactly how the rsphp-sys crate is built - the bindings are generated at build time based on the currently installed version of PHP. The default build script for the rsphp-sys crate can be found on docs.rs.

However, just the build script on its own does not solve everything - not only do different PHP installations have different configuration, but extensions might want to have bindings for different parts of PHP's C ABI. In the workshop I led, by the end the extension we were building needed to depend on PHP's built-in Date extension, in addition to the core Zend engine. I thus designed the rsphp-sys crate with the intention that even the build script might be overwritten, making use of Cargo's support for patching dependencies.

Customizing bindings

The bindings for PHP's ABI need extensive customization. For example, by default bindgen will try to convert C integer #define preprocessor constants into i32 or u32 types - in C, the constant doesn't need to specify a type. However, PHP uses a lot of C preprocessor constants, and many of those are intended to be used as other types, e.g. as a single byte (u8). While bindgen can be configured to use the smallest type that a constant will fit in, for a set of bitmask flags, all of the constants should be of a consistent type, even if some of the flags could fit into smaller integer types.

When an extension developer overrides the default build.rs script with their own, in order to customize what bindings are generated, they would presumably want to keep the existing customizations that the rsphp-sys crate comes with by default. To avoid needing to copy and paste a huge build script, I refactored as many of the customizations as possible into a separate crate, rsphp-build-tools.

The rsphp-sys crate lays the groundwork for PHP extensions written in Rust to interact with PHP's C ABI. The accompanying rsphp-build-tools crate makes it possible to customize those bindings without duplicating too much of the build-script logic. In the next post, I'll explore rsphp-build-tools in more detail.

Tags: