Writing a Basic PHP Extension in C
Build and load your own native extension.
Writing a Basic PHP Extension in C is a free PHP Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the PHP Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Native Code in PHP
When pure PHP is too slow or you need to bind a C library, you write a PHP extension in C against the Zend API. The extension exposes native functions/classes that PHP calls directly, with no VM overhead.
This lesson builds a minimal hello extension end-to-end: skeleton, function, build, load, and test.
The Build Toolchain
Extensions are built with PHP's phpize, which prepares an autoconf build using the headers of your installed PHP. You need php-dev/php-devel (provides phpize and php-config) plus a C compiler and make.
# Install build prerequisites (Debian/Ubuntu)
sudo apt install php-dev build-essential
# Confirm the tools exist
phpize --version
php-config --extension-dir # where the .so will be installedconfig.m4
Every extension needs a config.m4 that registers a build flag and declares the source files. phpize consumes it to generate the configure script.
dnl config.m4 for the 'hello' extension
PHP_ARG_ENABLE([hello],
[whether to enable hello support],
[AS_HELP_STRING([--enable-hello], [Enable hello])],
[no])
if test "$PHP_HELLO" != "no"; then
PHP_NEW_EXTENSION(hello, hello.c, $ext_shared)
fiExtension Headers
The C source includes the Zend/PHP headers and declares the module entry. php.h pulls in the core API; ext/standard/info.h is used for phpinfo() output. Every extension defines a zend_module_entry.
/* hello.c — includes */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "php.h"
#include "ext/standard/info.h"
#include "hello_arginfo.h" /* generated from stub */Arginfo Stubs
Modern PHP generates argument metadata from a .stub.php file. You write the function signature in PHP-like syntax; gen_stub.php produces hello_arginfo.h. This keeps reflection and type info accurate.
<?php
// hello.stub.php — describes the native function's signature
/** @generate-class-entries */
function hello_greet(string $name): string {}
?>Implementing the Function
A native function is a C function tagged with PHP_FUNCTION. You parse incoming arguments with ZEND_PARSE_PARAMETERS macros and return values via RETURN_* macros. Here we build a greeting string.
/* hello.c — the native function */
PHP_FUNCTION(hello_greet)
{
char *name;
size_t name_len;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_STRING(name, name_len)
ZEND_PARSE_PARAMETERS_END();
/* Build "Hello, <name>!" into a new zend_string */
zend_string *result = strpprintf(0, "Hello, %s!", name);
RETURN_STR(result); /* hands ownership to the engine */
}The Module Entry
The zend_module_entry ties everything together: name, version, the function table (from arginfo), and lifecycle hooks (MINIT, RINIT, MINFO). ZEND_GET_MODULE exports the entry symbol the loader looks up.
/* hello.c — module wiring */
zend_module_entry hello_module_entry = {
STANDARD_MODULE_HEADER,
"hello", /* extension name */
ext_functions, /* function table from arginfo */
NULL, /* MINIT (module startup) */
NULL, /* MSHUTDOWN */
NULL, /* RINIT (per-request) */
NULL, /* RSHUTDOWN */
PHP_MINFO(hello), /* phpinfo section */
"0.1.0",
STANDARD_MODULE_PROPERTIES
};
#ifdef COMPILE_DL_HELLO
ZEND_GET_MODULE(hello)
#endifMemory: emalloc vs malloc
Inside an extension, allocate request-lifetime memory with emalloc/efree (tracked by the Zend Memory Manager, freed at request end) — not raw malloc. For persistent (cross-request) allocations use pemalloc. Returning a zend_string via RETURN_STR transfers ownership to the engine, which frees it.
/* Request-scoped buffer the engine will clean up on error/shutdown */
char *buf = emalloc(64);
/* ... use buf ... */
efree(buf);
/* Persistent allocation surviving the request (rare) */
/* char *cfg = pemalloc(128, 1); ... pefree(cfg, 1); */Building It
The classic three-step build: phpize to scaffold, ./configure with your enable flag, then make. Remember to run gen_stub.php first to produce the arginfo header.
# Generate arginfo from the stub
php /path/to/php-src/build/gen_stub.php hello.stub.php
# Scaffold + configure + compile
phpize
./configure --enable-hello
make
# Result lands in modules/hello.so
ls -la modules/hello.soLoading & Testing
Load the compiled .so with -d extension=... (or add it to an ini file). Then call the native function from PHP exactly like a built-in. This is what your test/verification script looks like once the extension is installed.
<?php
// After: php -d extension=./modules/hello.so test.php
if (!extension_loaded('hello')) {
fwrite(STDERR, "hello extension not loaded\n");
exit(1);
}
echo hello_greet('Zend') . PHP_EOL; // Hello, Zend!
var_dump(extension_loaded('hello')); // bool(true)
?>When (Not) to Write One
Native extensions cost maintenance: C memory bugs, rebuilds per PHP minor version, and ABI breakage. Before going native, consider FFI (call C libraries from PHP without compiling an extension) or pure-PHP optimization. Reach for a C extension when you need maximum speed, deep engine integration, or to wrap a complex C library cleanly.
<?php
// FFI alternative: call a C library directly, no extension build
$ffi = FFI::cdef(
"int abs(int);", // declare the symbol
"libc.so.6"
);
echo $ffi->abs(-42) . PHP_EOL; // 42
?>Quick Check
Inside an extension, which allocator should hold request-lifetime memory?
Recap
You built a minimal C extension: config.m4 registers the build, a .stub.php generates arginfo, PHP_FUNCTION implements logic parsing args with ZEND_PARSE_PARAMETERS and returning via RETURN_STR, and a zend_module_entry wires lifecycle hooks. Build with phpize → configure → make, load the .so, and test from PHP. Use emalloc for request memory — and consider FFI before committing to native code.
Frequently asked questions
Is the “Writing a Basic PHP Extension in C” lesson free?
Yes — the full text of “Writing a Basic PHP Extension in C” is free to read here on the web, and the PHP Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the PHP Academy course, upgrade to CoddyKit PRO.
What will I learn in “Writing a Basic PHP Extension in C”?
Build and load your own native extension. You practise PHP Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start PHP Academy?
No prior experience is required. PHP Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Writing a Basic PHP Extension in C” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this PHP Academy lesson?
Yes. Every PHP Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- How the Zend Engine Works
- Memory Management and Garbage Collection
- OPcache and JIT Compilation
- Writing a Basic PHP Extension in C