ffi.php (9568B)
1 <?php 2 declare(strict_types=1); 3 namespace Arboricx; 4 5 final class TricuError extends \RuntimeException 6 { 7 public function __construct(public readonly int $status, string $message) 8 { 9 parent::__construct("tricu status $status: $message"); 10 } 11 } 12 13 function findLib(): string 14 { 15 $override = getenv('TRICU_LIB'); 16 if ($override !== false && $override !== '') { 17 $path = realpath($override); 18 if ($path === false || !is_file($path)) throw new \RuntimeException("TRICU_LIB not found: $override"); 19 return $path; 20 } 21 foreach (['libtricu-haskell.so', 'libtricu-haskell.dylib', 'tricu-haskell.dll'] as $name) { 22 $path = __DIR__ . '/../' . $name; 23 if (is_file($path)) return realpath($path); 24 } 25 throw new \RuntimeException('Set TRICU_LIB to the Haskell embedding library'); 26 } 27 28 final class Library 29 { 30 private static ?\FFI $native = null; 31 private static ?string $path = null; 32 public readonly \FFI $api; 33 public function __construct(?string $path = null) 34 { 35 if (PHP_INT_SIZE !== 8) throw new \RuntimeException('A 64-bit PHP build is required'); 36 $resolved = realpath($path ?? findLib()); 37 if ($resolved === false) throw new \RuntimeException('Library not found'); 38 if (self::$native !== null && self::$path !== $resolved) { 39 throw new \RuntimeException('One Haskell embedding library per process'); 40 } 41 if (self::$native === null) { 42 $header = false; 43 foreach ([__DIR__.'/..', __DIR__.'/../../haskell/include'] as $directory) { 44 if (is_file($directory.'/tricu.h')) { 45 $header = file_get_contents($directory.'/tricu.h'); 46 break; 47 } 48 } 49 if ($header === false) throw new \RuntimeException('Cannot read tricu headers'); 50 $header = preg_replace('/^#.*$/m', '', $header); 51 $header = preg_replace('/extern\s+"C"\s*\{/', '', $header); 52 $header = preg_replace('/^\}\s*$/m', '', $header); 53 $api = \FFI::cdef($header, $resolved); 54 $api->tricu_init(); 55 if ($api->tricu_abi_version() !== 1) throw new \RuntimeException('Haskell embedding ABI 1 required'); 56 self::$native = $api; 57 self::$path = $resolved; 58 } 59 $this->api = self::$native; 60 } 61 public function context(): Context { return new Context($this); } 62 } 63 64 final class Context 65 { 66 private readonly \FFI $api; 67 private readonly \FFI\CData $identity; 68 private bool $closed = false; 69 public function __construct(Library $library) 70 { 71 $this->api = $library->api; 72 $this->identity = $this->api->new('tricu_context'); 73 $status = $this->api->tricu_context_new(\FFI::addr($this->identity)); 74 if ($status !== 0) throw new TricuError($status, 'creating context'); 75 } 76 public function close(): void 77 { 78 if (!$this->closed) { 79 $this->check($this->api->tricu_context_free($this->identity->cdata)); 80 $this->closed = true; 81 } 82 } 83 // Explicit close is preferred; destruction also releases abandoned contexts. 84 public function __destruct() { if (!$this->closed) $this->api->tricu_context_free($this->identity->cdata); } 85 private function check(int $status): void 86 { 87 if ($status === 0) return; 88 $ptr = $this->api->new('uint8_t *'); 89 $len = $this->api->new('size_t'); 90 $diagnostic = $this->api->tricu_last_error($this->identity->cdata, \FFI::addr($ptr), \FFI::addr($len)); 91 $message = 'invalid context'; 92 if ($diagnostic === 0) { 93 try { $message = \FFI::string($this->api->cast('char *', $ptr), (int)$len->cdata); } 94 finally { $this->api->tricu_buffer_free($ptr); } 95 } 96 throw new TricuError($status, $message); 97 } 98 private function out(string $name, mixed ...$arguments): \FFI\CData 99 { 100 // FFI scalar calls take PHP scalar values. For uint64, PHP exposes the 101 // same 64 bits as a signed int; FFI converts those bits back to uint64. 102 $arguments = array_map(fn($arg) => $arg instanceof \FFI\CData && 103 \FFI::typeof($arg)->getKind() === \FFI\CType::TYPE_UINT64 ? $arg->cdata : $arg, $arguments); 104 $out = $this->api->new('uint64_t'); 105 $this->check($this->api->$name($this->identity->cdata, ...[...$arguments, \FFI::addr($out)])); 106 return $out; 107 } 108 public function release(\FFI\CData $value): void { $this->check($this->api->tricu_release($this->identity->cdata, $value->cdata)); } 109 public function retain(\FFI\CData $value): \FFI\CData { return $this->out('tricu_retain', $value); } 110 public function leaf(): \FFI\CData { return $this->out('tricu_leaf'); } 111 public function stem(\FFI\CData $value): \FFI\CData { return $this->out('tricu_stem', $value); } 112 public function fork(\FFI\CData $a, \FFI\CData $b): \FFI\CData { return $this->out('tricu_fork', $a, $b); } 113 public function apply(\FFI\CData $fn, \FFI\CData $arg): \FFI\CData { return $this->out('tricu_apply', $fn, $arg); } 114 public function reduce(\FFI\CData $value): \FFI\CData { return $this->out('tricu_reduce', $value); } 115 public function child(\FFI\CData $value, int $index): \FFI\CData 116 { 117 if ($index < 0 || $index > 0xffffffff) throw new \RangeException('invalid child index'); 118 return $this->out('tricu_child', $value, $index); 119 } 120 public function kind(\FFI\CData $value): int 121 { 122 $out = $this->api->new('uint32_t'); 123 $this->check($this->api->tricu_kind($this->identity->cdata, $value->cdata, \FFI::addr($out))); 124 return $out->cdata; 125 } 126 public function toBool(\FFI\CData $value): bool 127 { 128 $out = $this->api->new('uint32_t'); 129 $this->check($this->api->tricu_to_bool($this->identity->cdata, $value->cdata, \FFI::addr($out))); 130 return $out->cdata !== 0; 131 } 132 public function number(int|string $value): \FFI\CData 133 { 134 $text = (string)$value; 135 if (!preg_match('/^(0|[1-9][0-9]*)$/D', $text) || strlen($text) > 20 || 136 (strlen($text) === 20 && strcmp($text, '18446744073709551615') > 0)) { 137 throw new \RangeException('number must fit uint64'); 138 } 139 // PHP integers are signed. Byte-wise arithmetic preserves the full 140 // uint64 range when marshaling decimal input. 141 $bytes = array_fill(0, 8, 0); 142 foreach (str_split($text) as $digit) { 143 $carry = (int)$digit; 144 for ($i = 0; $i < 8; $i++) { 145 $n = $bytes[$i]*10+$carry; 146 $bytes[$i] = $n & 255; 147 $carry = $n >> 8; 148 } 149 } 150 if (pack('S', 1) !== "\x01\x00") $bytes = array_reverse($bytes); 151 $scalar = $this->api->new('uint64_t'); 152 \FFI::memcpy(\FFI::addr($scalar), pack('C*', ...$bytes), 8); 153 return $this->out('tricu_from_u64', $scalar); 154 } 155 public function toNumber(\FFI\CData $value): string 156 { 157 $out = $this->out('tricu_to_u64', $value); 158 $bytes = array_values(unpack('C*', \FFI::string($this->api->cast('char *', \FFI::addr($out)), 8))); 159 if (pack('S', 1) === "\x01\x00") $bytes = array_reverse($bytes); 160 $decimal = '0'; 161 foreach ($bytes as $byte) { 162 $carry = $byte; 163 $next = ''; 164 for ($i = strlen($decimal)-1; $i >= 0; $i--) { 165 $n = (int)$decimal[$i]*256+$carry; 166 $next = (string)($n % 10).$next; 167 $carry = intdiv($n, 10); 168 } 169 while ($carry > 0) { $next = (string)($carry % 10).$next; $carry = intdiv($carry, 10); } 170 $decimal = ltrim($next, '0') ?: '0'; 171 } 172 return $decimal; 173 } 174 private function input(string $value): \FFI\CData 175 { 176 $buffer = $this->api->new('uint8_t['.max(1, strlen($value)).']'); 177 if ($value !== '') \FFI::memcpy($buffer, $value, strlen($value)); 178 return $buffer; 179 } 180 public function bytes(string $value): \FFI\CData { return $this->out('tricu_from_bytes', $this->input($value), strlen($value)); } 181 public function string(string $value): \FFI\CData { return $this->out('tricu_from_utf8', $this->input($value), strlen($value)); } 182 private function buffer(string $name, \FFI\CData $value): string 183 { 184 $ptr = $this->api->new('uint8_t *'); $len = $this->api->new('size_t'); 185 $this->check($this->api->$name($this->identity->cdata, $value->cdata, \FFI::addr($ptr), \FFI::addr($len))); 186 try { return \FFI::string($this->api->cast('char *', $ptr), (int)$len->cdata); } 187 finally { $this->api->tricu_buffer_free($ptr); } 188 } 189 public function toBytes(\FFI\CData $value): string { return $this->buffer('tricu_to_bytes', $value); } 190 public function toString(\FFI\CData $value): string { return $this->buffer('tricu_to_utf8', $value); } 191 public function loadBundle(string $bytes, string $name): \FFI\CData 192 { 193 return $this->out('tricu_load_bundle', $this->input($bytes), strlen($bytes), $this->input($name), strlen($name)); 194 } 195 public function runIO(\FFI\CData $program): array 196 { 197 $outcome = $this->api->new('uint32_t'); 198 $value = $this->api->new('tricu_handle'); $cleanup = $this->api->new('tricu_handle'); 199 $this->check($this->api->tricu_run_io($this->identity->cdata, $program->cdata, 200 \FFI::addr($outcome), \FFI::addr($value), \FFI::addr($cleanup))); 201 return ['outcome' => $outcome->cdata, 'value' => $value, 'cleanup' => $cleanup]; 202 } 203 }