How to Write a PHP Extension with Zig?

Mateusz Charytoniuk - Apr 10 - - Dev Community

When writing code in a scripting language, sometimes you need that extra bit of performance (or maybe an async feature from Zig).

In that case, consider using it in your project.

Why Zig?

Zig is a statically typed, memory-safe, and thread-safe programming language.

It has a great interop with C, which makes it a perfect candidate for writing extensions for PHP. You don't have to worry about writing your bindings; you can call the functions from PHP internals straight up as they can also use C.

The Plan

We will still use a simple C facade to register Zig extension because Zig cannot translate all the C macros from the PHP source code into Zig code. You usually have to unwind them manually, so to avoid that, we will move as much of the extension initialization into C as possible.

  1. Create a basic C extension wrapper
  2. Fill in the gaps with Zig (statically linking functions)
  3. Phpize the extension
  4. Profit :)

We will create a basic "Hello from ZIG!" extension.

Basic C Extension Structure

All of those functions and entry points are mandatory.

In the header file, we will register myextension_functions as extern because they will be statically linked later.

#ifndef MYEXTENSION_H
#define MYEXTENSION_H
#define PHP_MY_PHP_EXTENSION_VERSION "1.0.0"

#include "php.h"

extern zend_function_entry myextension_functions[];

#endif
Enter fullscreen mode Exit fullscreen mode

As you can see, many C macros are Zig's weak (arguably) point.

I am internally conflicted about whether I should call that a "weak" point because Zig, by design, decided not to support any macros and that type of metaprogramming - I think rightfully so. On the other hand, that approach also makes it harder for Zig to interop with macro-heavy C libraries.

#include "hello.h"

PHP_MINIT_FUNCTION(my_php_extension) {
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(my_php_extension) {
    return SUCCESS;
}

PHP_RINIT_FUNCTION(my_php_extension) {
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(my_php_extension) {
    return SUCCESS;
}

PHP_MINFO_FUNCTION(my_php_extension) {
    php_info_print_table_start();
    php_info_print_table_header(2, "My PHP Extension", "enabled");
    php_info_print_table_end();
}

zend_module_entry my_php_extension_module_entry = {
    STANDARD_MODULE_HEADER,
    "my_php_extension",
    myextension_functions,
    PHP_MINIT(my_php_extension),
    PHP_MSHUTDOWN(my_php_extension),
    PHP_RINIT(my_php_extension),
    PHP_RSHUTDOWN(my_php_extension),
    PHP_MINFO(my_php_extension),
    PHP_MY_PHP_EXTENSION_VERSION,
    STANDARD_MODULE_PROPERTIES
};

ZEND_GET_MODULE(my_php_extension)
Enter fullscreen mode Exit fullscreen mode

You need a similar C structure, no matter what extension you intend to write.

Zig Part

That is the core of our extension. Here, we define our hello_world function that will be exported into PHP alongside its arguments and return type.

As you can see, we can use C functions directly with Zig code, which is great.

We can follow PHP internals documentation
here.

myextension_functions has to end with an empty terminator, hence the extra entry.

That is the basic extension that prints "Hello from ZIG!" but you can expand it with more complex features.

const std = @import("std");
const php = @cImport({
    @cInclude("php.h");
});

export fn hello_world(execute_data: ?*php.zend_execute_data, return_value: ?*php.zval) void {
    _ = execute_data;
    _ = return_value;
    _ = php.php_printf("Hello from ZIG!\n");
}

const arg_info = [_]php.zend_internal_arg_info{
    .{
        .name = null,
        .type = .{
            .type_mask = php.MAY_BE_NULL,
            .ptr = null,
        },
    },
};

export const myextension_functions = [_]php.zend_function_entry{
    php.zend_function_entry{
        .fname = "hello_world",
        .handler = hello_world,
        .arg_info = &arg_info,
        .num_args = 0,
        .flags = 0,
    },
    php.zend_function_entry{
        .fname = null,
        .handler = null,
        .arg_info = null,
        .num_args = 0,
        .flags = 0,
    },
};
Enter fullscreen mode Exit fullscreen mode

Here, we configure the Zig build system to build our extension as a static library.

I took the included paths from the php-config --includes command. If you feel like it, you can invoke it in the build system, so you don't have to spell out include paths explicitly.

/usr/include/x86_64-linux-gnu is a common include path for Linux distributions. It uses my system's include paths, so you might have to adjust them.

const std = @import("std");

pub fn build(b: *std.Build) void {
    const optimize = b.standardOptimizeOption(.{});
    const target = b.standardTargetOptions(.{});

    const library = b.addStaticLibrary(.{
        .name = "my_php_extension",
        .root_source_file = .{
            .path = "hello.zig",
        },
        .target = target,
        .optimize = optimize,
    });

    library.addIncludePath(.{
        .path = "/usr/include/php/20220829"
    });
    library.addIncludePath(.{
        .path = "/usr/include/php/20220829/main"
    });
    library.addIncludePath(.{
        .path = "/usr/include/php/20220829/TSRM"
    });
    library.addIncludePath(.{
        .path = "/usr/include/php/20220829/Zend"
    });
    library.addIncludePath(.{
        .path = "/usr/include"
    });
    library.addIncludePath(.{
        .path = "/usr/include/x86_64-linux-gnu"
    });

    b.installArtifact(library);
}
Enter fullscreen mode Exit fullscreen mode

With those two files in place, after running zig build, you should have zig-out/lib/libmy_php_extension.a file compiled, a static library that we will link later with the C part of the PHP extension.

PHP Part

To start with, we need Autoconf
configuration file for our extension.

PHP_ARG_ENABLE(my_php_extension, whether to enable My PHP Extension,
[ --enable-my-php_extension   Enable My PHP Extension support])

if test "$PHP_MY_PHP_EXTENSION" != "no"; then
    PHP_REQUIRE_CXX()
    PHP_ADD_LIBRARY(stdc++, 1, MY_PHP_EXTENSION_SHARED_LIBADD)
    PHP_SUBST(MY_PHP_EXTENSION_SHARED_LIBADD)
    PHP_ADD_LIBRARY_WITH_PATH(my_php_extension, ./zig-out/lib, MY_PHP_EXTENSION_SHARED_LIBADD)
    PHP_NEW_EXTENSION(my_php_extension, hello.c, $ext_shared)
fi
Enter fullscreen mode Exit fullscreen mode

The important line is

PHP_ADD_LIBRARY_WITH_PATH(my_php_extension, ./zig-out/lib, MY_PHP_EXTENSION_SHARED_LIBADD)
Enter fullscreen mode Exit fullscreen mode

Which tells the PHP build system to link the libmy_php_extension.a file from the Zig build.

Putting it All Together

Now that we have all the pieces, we can compile our extension.

Run the following commands:

  1. zig build - to build the Zig static library
  2. phpize - to prepare the PHP extension build
  3. ./configure - to configure the extension
  4. make - to compile the extension

After that, you should have a modules/my_php_extension.so file that you can load into your PHP.

To test if it works, you can run:

php -d extension=./modules/my_php_extension.so -r 'echo hello_world();'
Enter fullscreen mode Exit fullscreen mode

You should see Hello from ZIG!.

Congratulations! You have just written a PHP extension with Zig!

Conclusion

Zig is a great choice for writing extensions that require high performance or async features and can enrich your PHP experience.

. . . . . . . . . . . . . . . . . . . . . . . .