feat(php): use new FFI for Arboricx

This commit is contained in:
2026-05-11 09:18:47 -05:00
parent d7a7a8134c
commit d37d443021
8 changed files with 305 additions and 605 deletions

138
ext/php/src/ffi.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace Arboricx;
/**
* FFI wrapper around libarboricx.so.
*
* Loads the shared library and exposes typed wrappers for the C ABI.
*/
final class ArboricxFFI
{
private static ?\FFI $ffi = null;
public static function init(string $libPath): void
{
if (self::$ffi !== null) {
return;
}
// Nix output layout first, then repo layout.
$candidates = [
__DIR__ . '/../arboricx.h',
__DIR__ . '/../../zig/include/arboricx.h',
];
$headerRaw = false;
foreach ($candidates as $path) {
$headerRaw = file_get_contents($path);
if ($headerRaw !== false) break;
}
if ($headerRaw === false) {
throw new \RuntimeException('Cannot read arboricx.h');
}
// PHP FFI only parses plain C declarations.
$header = $headerRaw;
$header = preg_replace('/#.*\n/', "\n", $header);
$header = preg_replace('/extern\s+"C"\s*\{/', '', $header);
$header = str_replace('}', '', $header);
$header = preg_replace('/\n\s*\n+/', "\n", $header);
self::$ffi = \FFI::cdef($header, $libPath);
}
public static function ffi(): \FFI
{
if (self::$ffi === null) {
throw new \RuntimeException('ArboricxFFI not initialized. Call ArboricxFFI::init($libPath) first.');
}
return self::$ffi;
}
}
function ctx_init(string $libPath): \FFI\CData
{
ArboricxFFI::init($libPath);
$ctx = ArboricxFFI::ffi()->arboricx_init();
if ($ctx === null) {
throw new \RuntimeException('arboricx_init failed');
}
return $ctx;
}
function ctx_free(\FFI\CData $ctx): void
{
ArboricxFFI::ffi()->arboricx_free($ctx);
}
function app(\FFI\CData $ctx, int $func, int $arg): int
{
return ArboricxFFI::ffi()->arb_app($ctx, $func, $arg);
}
function reduce(\FFI\CData $ctx, int $root, int $fuel = 1_000_000_000): int
{
return ArboricxFFI::ffi()->arb_reduce($ctx, $root, $fuel);
}
function ofNumber(\FFI\CData $ctx, int $n): int
{
return ArboricxFFI::ffi()->arb_of_number($ctx, $n);
}
function ofString(\FFI\CData $ctx, string $s): int
{
return ArboricxFFI::ffi()->arb_of_string($ctx, $s);
}
function toNumber(\FFI\CData $ctx, int $root): int
{
$out = ArboricxFFI::ffi()->new('uint64_t');
$ok = ArboricxFFI::ffi()->arb_to_number($ctx, $root, \FFI::addr($out));
if (!$ok) {
throw new \RuntimeException('arb_to_number failed');
}
return (int) $out->cdata;
}
function toString(\FFI\CData $ctx, int $root): string
{
$ptr = ArboricxFFI::ffi()->new('uint8_t*');
$len = ArboricxFFI::ffi()->new('size_t');
$ok = ArboricxFFI::ffi()->arb_to_string($ctx, $root, \FFI::addr($ptr), \FFI::addr($len));
if (!$ok) {
throw new \RuntimeException('arb_to_string failed');
}
$length = (int) $len->cdata;
$result = '';
for ($i = 0; $i < $length; $i++) {
$result .= chr($ptr[$i]);
}
ArboricxFFI::ffi()->arboricx_free_buf($ctx, $ptr, $length);
return $result;
}
function toBool(\FFI\CData $ctx, int $root): bool
{
$out = ArboricxFFI::ffi()->new('int');
$ok = ArboricxFFI::ffi()->arb_to_bool($ctx, $root, \FFI::addr($out));
if (!$ok) {
throw new \RuntimeException('arb_to_bool failed');
}
return (bool) $out->cdata;
}
function loadBundleDefault(\FFI\CData $ctx, string $bytes): int
{
$cdata = ArboricxFFI::ffi()->new('uint8_t[' . strlen($bytes) . ']');
for ($i = 0; $i < strlen($bytes); $i++) {
$cdata[$i] = ord($bytes[$i]);
}
$result = ArboricxFFI::ffi()->arb_load_bundle_default($ctx, $cdata, strlen($bytes));
if ($result === 0) {
throw new \RuntimeException('arb_load_bundle_default failed');
}
return $result;
}