0Pricing
PHP Academy · Lesson

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 installed

config.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)
fi

Extension 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)
#endif

Memory: 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.so

Loading & 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 phpizeconfiguremake, 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

  1. How the Zend Engine Works
  2. Memory Management and Garbage Collection
  3. OPcache and JIT Compilation
  4. Writing a Basic PHP Extension in C
← Back to PHP Academy