Skip to main content
  1. Programming Languages/
  2. Modern PHP Development/

Unlocking Raw Performance: Writing Custom PHP Extensions in C

Jeff Taakey
Author
Jeff Taakey
21+ Year CTO & Multi-Cloud Architect. Bridging the gap between theoretical CS and production-grade engineering for 300+ deep-dive guides.

Introduction
#

In the landscape of 2025, PHP is faster than ever. With the maturity of PHP 8.4, the optimizations in the JIT (Just-In-Time) compiler, and the improved type system, the need to drop down to C is less frequent than it was a decade ago. However, “less frequent” does not mean “obsolete.”

There are still specific scenarios where userland PHP—even with JIT—hits a ceiling. Whether you are interfacing with a legacy C library, performing heavy mathematical computations that require SIMD instructions, or simply need to optimize a bottleneck loop that runs billions of times, writing a custom PHP extension remains the ultimate weapon in a developer’s arsenal.

This is not about replacing your entire codebase with C. It’s about surgical precision. In this guide, we will bypass the abstractions and dive into the Zend Engine. You will learn how to set up a development environment, scaffold an extension, write C code that interacts with PHP variables, and compile it into a .so file that you can load in your php.ini.

Prerequisites and Environment Setup
#

Before we write a single line of C code, we need a stable environment. Developing PHP extensions on Windows is possible but painful due to compiler variations. We highly recommend a Linux environment (Ubuntu 22.04/24.04 or Debian 12). If you are on macOS or Windows, Docker is mandatory to follow this guide without headaches.

Essential Tools
#

You will need the PHP source development tools. In 2025, we are targeting PHP 8.3 or 8.4.

If you are using Ubuntu/Debian, install the build essentials:

sudo apt-get update
sudo apt-get install -y \
    build-essential \
    autoconf \
    bison \
    libxml2-dev \
    php8.3-dev \
    php8.3-cli

The Workflow Visualization
#

Understanding the build lifecycle is crucial before we start. Here is how a piece of C code becomes a callable PHP function:

flowchart TD A["Source Code (.c / .h)"] -->|phpize| B["Prepare Build Environment"] B -->|"./configure"| C["Makefile Generation"] C -->|make| D["Compilation Object Files (.o)"] D -->|Linker| E["Shared Object (.so)"] E -->|"php.ini"| F["PHP Runtime"] subgraph "Zend Engine" F --> G["PHP Function Registry"] end style A fill:#f9f,stroke:#333,stroke-width:2px style E fill:#bbf,stroke:#333,stroke-width:2px style F fill:#bfb,stroke:#333,stroke-width:2px

Step 1: Scaffolding the Extension
#

Gone are the days of manually writing config.m4 files from scratch. PHP ships with a tool called ext_skel (Extension Skeleton) that generates the boilerplate for us.

Let’s create an extension named supermath. We will build a simple but computationally expensive function: calculating the factorial of large numbers, but optimizing it in C.

Navigate to your working directory and run:

# If you downloaded the PHP source code:
cd php-src/ext
./ext_skel.php --ext supermath

# If you are using the installed php-dev package tools (common location):
# You might need to locate ext_skel.php usually in /usr/lib/php/build/ or similar
# Or simply download the script from the official PHP git repo.

Note: For this tutorial, we assume you are working in a standalone folder structure.

The skeleton generator creates the following structure:

supermath/
├── config.m4       # UNIX build configuration
├── config.w32      # Windows build configuration
├── php_supermath.h # Header file
├── supermath.c     # Main source file
└── tests/          # .phpt test files

Step 2: Configuring the Build System
#

The config.m4 file is used by phpize to generate the configure script. It tells the build system how to compile your extension.

Open supermath/config.m4. You will usually see commented-out sections. You need to uncomment the section that enables your extension.

Change this:

dnl PHP_ARG_ENABLE(supermath, whether to enable supermath support,
dnl [  --enable-supermath           Enable supermath support])

To this (remove dnl which stands for “delete to new line” - effectively a comment):

PHP_ARG_ENABLE(supermath, whether to enable supermath support,
[  --enable-supermath           Enable supermath support])

if test "$PHP_SUPERMATH" != "no"; then
  AC_DEFINE(HAVE_SUPERMATH, 1, [ Whether you have supermath ])
  PHP_NEW_EXTENSION(supermath, supermath.c, $ext_shared)
fi

Step 3: The C Implementation
#

Now, the fun part. Open supermath.c.

This file contains the module entry point and the function definitions. We are going to add a function called supermath_factorial.

Understanding Zend Arguments
#

To receive arguments from PHP, we use ZEND_PARSE_PARAMETERS. This macro handles type checking automatically. If a user passes a string where an integer is expected, this macro triggers a standard PHP TypeError.

Find the PHP_FUNCTION section in supermath.c and add our new function:

/* supermath.c */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "php.h"
#include "ext/standard/info.h"
#include "php_supermath.h"

/* The logic in pure C */
long long calculate_factorial(long n) {
    if (n < 0) return 0;
    if (n == 0) return 1;
    long long result = 1;
    for (long i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}

/* 
 * The PHP Wrapper 
 * usage: int supermath_factorial(int $number)
 */
PHP_FUNCTION(supermath_factorial)
{
    long number;

    // "l" indicates we expect a Long (integer)
    if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &number) == FAILURE) {
        RETURN_THROWS();
    }

    if (number < 0) {
        zend_throw_exception(zend_ce_error, "Negative numbers not allowed", 0);
        RETURN_THROWS();
    }

    // Call internal C function
    long long result = calculate_factorial(number);

    // Return the result to PHP
    RETURN_LONG(result);
}

Registering the Function
#

Defining the function isn’t enough; we must register it in the function table so PHP knows it exists.

Look for zend_function_entry supermath_functions[]:

/* supermath.c continued */

static const zend_function_entry supermath_functions[] = {
    PHP_FE(supermath_factorial, arginfo_supermath_factorial) 
    PHP_FE_END
};

Wait! We haven’t defined arginfo_supermath_factorial yet. PHP 8 requires “Argument Info” for reflection and type safety.

Add this near the top of the file (or in the header if you prefer), usually before the zend_function_entry:

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_supermath_factorial, 0, 1, IS_LONG, 0)
    ZEND_ARG_TYPE_INFO(0, number, IS_LONG, 0)
ZEND_END_ARG_INFO()

This macro magic tells PHP:

  1. Function name: supermath_factorial
  2. Required arguments: 1
  3. Return type: IS_LONG
  4. Argument 1 name: number, Type: IS_LONG.

Step 4: Compiling and Installing
#

Now we move to the terminal. Run the following commands inside your supermath directory:

  1. Prepare the environment:

    phpize

    Output should look like: Configuring for: PHP Api Version: 20230831...

  2. Configure:

    ./configure --enable-supermath
  3. Compile:

    make
  4. Install:

    sudo make install

The make install command will copy the resulting .so file to your PHP extensions directory. It will tell you the path, usually something like /usr/lib/php/20230831/.

Step 5: Enabling and Testing
#

You don’t need to edit the global php.ini just to test. You can pass the extension via the command line.

Create a test script test.php:

<?php

if (!extension_loaded('supermath')) {
    die("Extension not loaded!\n");
}

echo "Calculating factorial of 10 using C:\n";
$start = microtime(true);
$result = supermath_factorial(10);
$end = microtime(true);

echo "Result: " . $result . "\n";
echo "Time: " . ($end - $start) . " seconds\n";

try {
    supermath_factorial(-5);
} catch (Error $e) {
    echo "Successfully caught error: " . $e->getMessage() . "\n";
}

Run it:

php -d extension=supermath.so test.php

If you see the output, congratulations! You have successfully extended PHP with C.

Performance Analysis: Native vs. Extension
#

Why go through all this trouble? Let’s look at the data. While simple math is fast in PHP, let’s consider a scenario involving heavy string manipulation or nested loops (like image processing or complex parsing).

Here is a comparison of different approaches available in modern PHP ecosystems.

Approach Setup Difficulty Execution Speed Maintenance Cost Use Case
Native PHP Low Fast (w/ JIT) Low 95% of web logic
PHP Extension (C) High Fastest (Native) High Low-level drivers, Heavy algo
PHP FFI Medium High Medium calling existing C libs
Go/Rust Microservice Medium High High (Network latency) Distributed tasks

A Note on Memory Management
#

When writing C for PHP, you generally shouldn’t use malloc and free. Instead, use the Zend Memory Manager (ZMM):

  • emalloc(size) instead of malloc(size)
  • efree(ptr) instead of free(ptr)
  • estrdup(str) instead of strdup(str)

ZMM automatically frees memory at the end of the request if you forget to (preventing persistent memory leaks), and it works with PHP’s resource limits.

Common Pitfalls for Beginners
#

1. The Thread Safety (TSRM) Trap
#

PHP can run in Thread Safe (ZTS) or Non-Thread Safe (NTS) modes. If you use global variables in your C code, they will be shared across requests in a threaded environment (like Apache Worker MPM), leading to race conditions. Solution: Always use the PHP_G() macro and define globals within the standard PHP structure. Avoid static C variables unless they are read-only.

2. String Handling
#

PHP strings are not simple C strings (char *). They are zend_string structures that contain the length. Solution: Never assume a string is null-terminated if you are accessing the raw buffer directly, although Zend usually adds a null terminator for convenience. Always respect ZSTR_LEN(str).

3. Build Mismatches
#

Trying to load an extension compiled against PHP 8.3 into a PHP 8.4 runtime will fail instantly. Solution: Always compile the extension on the exact same system (or Docker container) where it will run.

Conclusion
#

Creating a PHP extension is a powerful skill. It forces you to understand how PHP works “under the hood”—how variables are counted (refcounting), how memory is managed, and how functions are dispatched.

In 2025, you might not write an extension for every project. However, knowing how to wrap a proprietary C library or how to optimize a critical path using ext_skel distinguishes a Senior Developer from a true PHP Architect.

Further Reading
#

  • PHP Internals Book: The unofficial but definitive guide.
  • Zend API Source: Look at Zend/zend_API.h in the PHP source code.
  • FFI: If you just need to call a C function without compiling a whole extension, check out PHP’s Foreign Function Interface.

Ready to optimize? Spin up that Docker container and start compiling.