This repository demonstrates building PHP extensions using two different approaches:
- FrankenPHP (Go-based) - Modern extension development using Go
- Traditional C - Classic PHP extension development
This project was created for a talk comparing these two approaches to PHP extension development.
.
βββ go-version/ # FrankenPHP extension built with Go
βββ c-version/ # Traditional PHP extension built with C
βββ compose.yml # Docker Compose configuration for both versions
Both versions implement the same functionality to demonstrate equivalent capabilities:
-
repeat_this(string $str, int $count, bool $reverse): string- Repeats a string
$counttimes - Optionally reverses the result (UTF-8 aware)
- Example:
repeat_this("Hi", 3, false)β"HiHiHi" - Example:
repeat_this("Hi", 3, true)β"iHiHiH"
- Repeats a string
-
matrix_multiply(array $matrix1, array $matrix2): array- Multiplies two matrices
- C version uses CBLAS for optimized performance
- Go version uses custom matrix multiplication with cache-optimized transposition
DirectoryScanner (Go version only)
setPath(string $path): bool- Set the directory to scanscan(): array- Recursively scan directory and return all file pathsscanConcurrently(): array- Same as scan but uses Go goroutines for concurrent traversal
- Docker
- Docker Compose
Build the service you want to work with:
# Build the C extension
docker compose build cext
# Build the Go/FrankenPHP extension
docker compose build goext# Enter the container
docker compose run --rm -it cext bash
# Inside the container, run the test file
php test.phpThe C extension is pre-compiled and loaded via php.ini.
# Enter the container
docker compose run --rm -it goext bash
# Inside the container, run test files using frankenphp
frankenphp php-cli php-tests/repeat.php
frankenphp php-cli php-tests/matrix_multiply.php
frankenphp php-cli php-tests/directory_scan.phpLocated in go-version/sdphp/stringext.go
Special Comment Directives: FrankenPHP uses special comments to export Go code to PHP:
// export_php:function repeat_this(string $str, int $count, bool $reverse): string
func repeat_this(s *C.zend_string, count int64, reverse bool) unsafe.Pointer {
// Implementation
}
// export_php:class DirectoryScanner
type DirectoryScanner struct {
Path string
}
// export_php:method DirectoryScanner::setPath(string $path): bool
func (ds *DirectoryScanner) SetPath(path *C.zend_string) bool {
// Implementation
}CGo Integration: The Go code uses CGo to interact with PHP's C API:
/*
#include <Zend/zend_types.h>
#include <Zend/zend_hash.h>
#include <stdlib.h>
#include "stringext.h"
#include "helper.h"
HashTable* create_matrix_array(long rows, long cols, long* data);
HashTable* create_string_array(char** strings, long count);
*/
import "C"Type Conversions: FrankenPHP provides helper functions to convert between Go and PHP types:
frankenphp.GoString()- Convert PHP string to Go stringfrankenphp.PHPString()- Convert Go string to PHP stringfrankenphp.GoPackedArray()- Convert PHP array to Go slice
The build process (build-ext.sh) includes several workarounds for FrankenPHP bugs (as of v1.9.1):
- C Preamble Restoration: The code generator removes C preamble comments, breaking extensions that reference C functions. The script extracts and re-inserts the preamble.
# Get our preamble
sed -n '1,/^import "C"$/p' $EXT_PATH/stringext.go > /tmp/preamble.txt
# Remove generated preamble and prepend our preamble
sed '1,/^import "C"$/d' $EXT_PATH/build/stringext.go | cat /tmp/preamble.txt - > /tmp/stringext_fixed.go- Type Mismatch Fix: FrankenPHP incorrectly generates
(int)pathtype hint for theSetPathmethod wrapper when it should bezend_string*:
# Fix the incorrect (int) typecast to allow proper zend_string* passing
sed -i 's/(int)path/path/g' $EXT_PATH/build/stringext.c- Missing Import: The generated code relies on
runtime/cgobut doesn't import it:
# Automatically add missing imports using goimports
go install golang.org/x/tools/cmd/goimports@latest
goimports -w $EXT_PATH/build/stringext.go- Module Setup: Copy necessary files and run
go mod tidyto ensure all dependencies are resolved:
cp $EXT_PATH/go.mod $EXT_PATH/go.sum $EXT_PATH/helper.h $EXT_PATH/helper.c $EXT_PATH/build/
cd $EXT_PATH/build && go mod tidyThese workarounds are necessary due to FrankenPHP being experimental for extension development. Future versions may resolve these issues.
Located in c-version/sdphp/sdphp.c
Function Registration: Functions are registered via stub files and arginfo:
- Define function signature in
sdphp.stub.php:
function repeat_this(string $str, int $count, bool $reverse): string {}-
Generate arginfo using PHP's
gen_stub.php(createssdphp_arginfo.h) -
Implement the function using
PHP_FUNCTIONmacro:
PHP_FUNCTION(repeat_this)
{
zend_string *str;
zend_long count;
zend_bool reverse;
ZEND_PARSE_PARAMETERS_START(3, 3)
Z_PARAM_STR(str)
Z_PARAM_LONG(count)
Z_PARAM_BOOL(reverse)
ZEND_PARSE_PARAMETERS_END();
// Implementation
}UTF-8 String Handling: The C version includes custom UTF-8 character boundary detection for proper string reversal:
// Detect UTF-8 character length by examining the first byte
static size_t utf8_char_len(const char *s) {
unsigned char c = (unsigned char)*s;
if (c < 0x80) return 1; // 0xxxxxxx - ASCII
if (c < 0xC0) return 1; // Invalid/continuation byte
if (c < 0xE0) return 2; // 110xxxxx - 2 bytes
if (c < 0xF0) return 3; // 1110xxxx - 3 bytes
if (c < 0xF8) return 4; // 11110xxx - 4 bytes
return 1;
}
// Two-pass reversal: reverse bytes, then fix multi-byte chars
static void utf8_reverse(char *str, size_t len) {
// First pass: reverse all bytes
// Second pass: reverse bytes within each multi-byte character
}This ensures proper handling of multi-byte UTF-8 characters (emoji, Chinese, etc.) when reversing strings.
CBLAS Integration: The matrix multiplication uses optimized BLAS routines:
cblas_dgemm(
CblasRowMajor, // Row-major order
CblasNoTrans, // Don't transpose A
CblasNoTrans, // Don't transpose B
m, p, n, // Dimensions
1.0, // Alpha scaling
A_flat, n, // Matrix A
B_flat, p, // Matrix B
0.0, // Beta scaling
C_flat, p // Result matrix C
);- Modern Language: Use Go's standard library (goroutines, channels, etc.)
- Memory Safety: Go's garbage collector handles memory management
- Easier Development: Higher-level abstractions, better tooling
- Concurrency: Built-in goroutines for parallel operations
- Less Boilerplate: Simpler type conversions and error handling
- Mature Ecosystem: Well-documented, stable API
- Performance: Direct memory manipulation, no GC overhead
- Fine-grained Control: Complete control over memory and optimization
- No Workarounds: Established toolchain without experimental bugs
- Smaller Binary: No Go runtime overhead
FrankenPHP extension development is experimental and requires workarounds for:
- Code generation bugs (C preamble removal)
- Type system issues (incorrect type hints)
- Missing imports (runtime/cgo)
- Documentation gaps
These issues will likely be resolved in future versions as FrankenPHP matures.
C Version: Manual memory management with malloc/free. Requires careful cleanup to avoid leaks.
Go Version: Garbage collected. Memory is automatically managed, but GC pauses may occur.
Both versions implement in-place UTF-8 aware reversal with O(1) space complexity and O(n) time complexity. The algorithm:
- Reverse all bytes
- Iterate through and reverse bytes within each multi-byte UTF-8 character
This preserves multi-byte characters (like emoji: π, Chinese: δΈη) correctly.
C Version: Uses CBLAS cblas_dgemm - highly optimized, industry-standard BLAS implementation.
Go Version: Custom implementation with cache-optimized matrix transposition. Pre-transposes the second matrix for better cache locality during multiplication.
- FrankenPHP Documentation
- PHP Internals Book
- PHP Extension Development (Traditional C)
- Zend API Documentation
This is demonstration code for educational purposes.