Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,28 @@ jobs:
- run: ./tests/wordpress/setup
- run: ./tests/wordpress/run

extensions:
needs: changes
if: needs.changes.outputs.run-tests == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: mlugg/setup-zig@v2
with:
version: 0.15.1
- name: install system libs
timeout-minutes: 5
env:
DEBIAN_FRONTEND: noninteractive
run: |
sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|https://archive.ubuntu.com/ubuntu|g' /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true
sudo apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20 update
sudo apt-get install -y libpcre2-dev libsqlite3-dev zlib1g-dev libmysqlclient-dev libpq-dev libssl-dev libnghttp2-dev libcurl4-openssl-dev libxml2-dev libicu-dev libgmp-dev libgd-dev libsodium-dev libldap2-dev
- run: zig build
- name: Run ./tests/extensions/run (dynamic and static)
run: STATIC=1 ./tests/extensions/run

compile:
needs: changes
if: needs.changes.outputs.run-tests == 'true'
Expand Down
10 changes: 7 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ PKG_CONFIG_PATH := /opt/homebrew/opt/mysql-client/lib/pkgconfig:/opt/homebrew/op
export PKG_CONFIG_PATH

.PHONY: build
build: ## Build zphp (Debug; ~30x slower than release due to Zig's debug allocator stack-trace capture). use `make release` for benchmarking
zig build
build: ## Build zphp (Debug; ~30x slower than release due to Zig's debug allocator stack-trace capture). use `make release` for benchmarking. ZIG_FLAGS adds build options such as -Dextension=path.c
zig build $(ZIG_FLAGS)

.PHONY: release
release: ## Build zphp in ReleaseFast (no debug allocator overhead). prefer for any perf-sensitive run; for testing zphp's actual PHP-execution speed
zig build -Doptimize=ReleaseFast
zig build -Doptimize=ReleaseFast $(ZIG_FLAGS)

.PHONY: test
test: ## Run zig unit tests
Expand Down Expand Up @@ -40,6 +40,10 @@ bench-compare: ## Time this tree against its merge base with main on this machin
fuzz: build ## Mutation-fuzz the pipeline and decoders on the Debug build (FUZZ_SECONDS per fuzzer, default 120)
python3 ./tests/fuzz/run all $(or $(FUZZ_SECONDS),120)

.PHONY: ext
ext: build ## Run extension API tests (builds tests/extensions/demo.c with zig cc; STATIC=1 also builds a zphp with it compiled in, BENCH=1 prints call overhead)
./tests/extensions/run

.PHONY: soak
soak: ## Run the memory soak (ReleaseFast): a string-heavy loop must hold a flat RSS
zig build -Doptimize=ReleaseFast
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ The glibc and macOS builds run on the platform they were made for; the musl buil

See the [documentation](https://nvms.github.io/zphp/) for build instructions and usage guides.

## Extensions

zphp loads native extensions written against a C ABI. An extension registers functions, classes, constants, ini defaults, and resource types once at load time, and every PHP call into it goes straight to a resolved function pointer. The header is `include/zphp_extension.h`; the same source builds as a shared library or gets compiled into zphp.

```c
#include "zphp_extension.h"

static void hello_add(zphp_ctx *ctx) {
zphp_return_int(ctx, zphp_get_int(zphp_arg(ctx, 0)) + zphp_get_int(zphp_arg(ctx, 1)));
}

static int module_init(zphp_module *m) {
return zphp_register_function(m, "hello_add", hello_add);
}

static const zphp_extension hello = {
.abi = ZPHP_EXTENSION_ABI, .name = "hello", .version = "1.0.0", .module_init = module_init,
};

ZPHP_EXTENSION(hello, &hello)
```

```sh
zig cc -shared -O2 -I include -o hello.so hello.c
zphp --extension=hello.so run app.php # dynamic
zig build -Doptimize=ReleaseFast -Dextension=hello.c # static, compiled into zphp
```

`ZPHP_EXTENSION_DIR` names a directory whose libraries load automatically. Extensions get module, worker, and request lifecycle hooks, a request-local and a worker-local data slot, and a destructor per resource type that runs when the PHP value is unset, goes out of scope, unwinds through an exception, or the request ends. Values cross the boundary as opaque handles, so the runtime's internals can change without breaking compiled extensions; an ABI version in the descriptor rejects mismatches at load time. The static musl release binaries cannot load shared libraries and take static extensions only. `tests/extensions/demo.c` exercises the whole API.

## Project Status

zphp was originally developed and maintained heavily through AI-assisted development.
Expand Down
39 changes: 39 additions & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ pub fn build(b: *std.Build) void {
.optimize = optimize,
});

// static extensions: C sources compiled into the binary. the generated
// module lists their entry points so the loader finds them without dlopen
const extension_sources = b.option([]const []const u8, "extension", "C source of a static extension, repeatable; the file stem is the extension name") orelse &.{};
const static_extensions = staticExtensionsModule(b, extension_sources);
exe_mod.addImport("static_extensions", static_extensions);
fast_loop_mod.addImport("static_extensions", static_extensions);
for (extension_sources) |source| {
exe_mod.addCSourceFile(.{ .file = .{ .cwd_relative = source }, .flags = &.{ "-std=c11", "-DZPHP_STATIC_EXTENSION" } });
}
exe_mod.addIncludePath(b.path("include"));

exe_mod.linkSystemLibrary("pcre2-8", .{ .preferred_link_mode = .static });
exe_mod.linkSystemLibrary("sqlite3", .{ .preferred_link_mode = .static });
exe_mod.linkSystemLibrary("z", .{ .preferred_link_mode = .static });
Expand Down Expand Up @@ -84,6 +95,7 @@ pub fn build(b: *std.Build) void {
.target = target,
.optimize = optimize,
});
test_mod.addImport("static_extensions", static_extensions);

test_mod.linkSystemLibrary("pcre2-8", .{ .preferred_link_mode = .static });
test_mod.linkSystemLibrary("sqlite3", .{ .preferred_link_mode = .static });
Expand Down Expand Up @@ -241,6 +253,33 @@ fn archiveIn(b: *std.Build, dir: []const u8, file: []const u8) ?[]const u8 {
return path;
}

fn staticExtensionsModule(b: *std.Build, sources: []const []const u8) *std.Build.Module {
var code = std.ArrayListUnmanaged(u8){};
const w = code.writer(b.allocator);
w.writeAll("pub const Entry = *const fn (*const anyopaque) callconv(.c) ?*const anyopaque;\n") catch @panic("OOM");
w.writeAll("pub const StaticExtension = struct { name: []const u8, entry: Entry };\n") catch @panic("OOM");
for (sources) |source| {
const stem = std.fs.path.stem(source);
if (!validIdentifier(stem)) std.debug.panic("-Dextension={s}: the file stem must be a C identifier, it names the extension entry", .{source});
w.print("extern fn zphp_extension_entry_{s}(api: *const anyopaque) callconv(.c) ?*const anyopaque;\n", .{stem}) catch @panic("OOM");
}
w.writeAll("pub const entries = [_]StaticExtension{") catch @panic("OOM");
for (sources) |source| {
const stem = std.fs.path.stem(source);
w.print(" .{{ .name = \"{s}\", .entry = &zphp_extension_entry_{s} }},", .{ source, stem }) catch @panic("OOM");
}
w.writeAll(" };\n") catch @panic("OOM");
const files = b.addWriteFiles();
const path = files.add("static_extensions.zig", code.items);
return b.createModule(.{ .root_source_file = path });
}

fn validIdentifier(s: []const u8) bool {
if (s.len == 0 or std.ascii.isDigit(s[0])) return false;
for (s) |ch| if (!(std.ascii.isAlphanumeric(ch) or ch == '_')) return false;
return true;
}

fn pkgConfigVariable(b: *std.Build, pkg: []const u8, name: []const u8) ?[]const u8 {
const arg = std.fmt.allocPrint(b.allocator, "--variable={s}", .{name}) catch return null;
const r = std.process.Child.run(.{
Expand Down
Loading
Loading