php-polycall
v1.1.0
Published
PHP FFI binding for the Polycall binding ABI v1 (libpolycall >= 1.1.0): config validation, RPC calls and peer nodes
Maintainers
Readme
php-polycall
PHP FFI binding for the Polycall
core's binding ABI v1 (polycall.h, libpolycall >= 1.1.0): configuration
validation, polycall_rpc calls and peer-to-peer nodes, from PHP through
ext-ffi. Composer package obinexus/php-polycall; npm distribution
php-polycall (not yet published).
The binding declares the exact C signatures from polycall.h
(Polycall::C_DEFINITION) and resolves every symbol when the library is
loaded. Configuration parsing, the wire protocols and the peer transport all
stay in the core.
Requirements
- PHP >= 8.1 with
ext-ffi(the CLI's defaultffi.enable=preloadallows FFI from CLI scripts; setffi.enable=1for other SAPIs) - the Polycall core >= 1.1.0 installed (
libpolycall.so.1,polycall.dll/libpolycall.dll, orlibpolycall.1.dylib)
QA status: tested on Linux x86_64 (Docker php:8.3-cli NTS and
php:8.3-zts with ext-parallel) against the real core. The Windows
(polycall.dll / libpolycall.dll) and macOS loader paths are implemented
but have not been run in QA yet (no PHP on the Windows QA host, no macOS
host).
Loading the library
new Polycall() tries, in order:
POLYCALL_LIBRARY(an explicit file path; a path that does not exist is an error, not a fallback),- the platform name through the OS loader search path:
polycall.dllthenlibpolycall.dll(Windows),libpolycall.so.1(Linux, viaLD_LIBRARY_PATH/ ldconfig),libpolycall.1.dylib(macOS).
No package or parent directory is searched. A missing library, a library
without the ABI v1 symbols (a 1.0 core) or polycall_ffi_abi_version() != 1
raises LibraryException naming the library and the problem.
API
use OBINexus\Polycall\Polycall;
use OBINexus\Polycall\PolycallException;
$pc = new Polycall(); // or new Polycall('/path/to/libpolycall.so.1')
$pc->abiVersion(); // 1
$pc->version(); // "1.1.0"
// configuration: the documented entry point, polycall_ffi_run_config(path, 1)
$status = $pc->runConfig('php-polycallrc'); // 0 = POLYCALL_OK, raw status otherwise
$pc->runConfig('php-polycallrc', false); // run=0: validate, unknown keys are warnings
$pc->runConfigOrThrow('php-polycallrc'); // throws PolycallException
$json = $pc->describe('php-polycallrc');
// one polycall_rpc round trip to `polycall start` / `polycall daemon start`
$out = $pc->call('127.0.0.1:8084', 'inventory', 'get', '{"item_id":"widget-a"}', 2000);
// peer nodes
$alpha = $pc->openPeer('alpha', '127.0.0.1:0', getenv('POLYCALL_DEV_TOKEN') ?: null);
$alpha->register('beta', '127.0.0.1:9002');
$alpha->send('beta', "binary\0safe", 'msg-1', 5000);
$message = $alpha->recv(5000); // Message {sender, messageId, payload}
$alpha->ping('beta');
$alpha->list(); $alpha->health(); $alpha->endpoint(); $alpha->nodeId();
$alpha->unregister('beta'); $alpha->cancel(); $alpha->close();Every failure throws PolycallException: status() / getCode() is the
POLYCALL_E_* code (constants in Status), statusName() and
statusText() come from polycall_strerror, detail() is
polycall_last_error, and output() holds the remote error object of a
failed call(). recv() with a too-small capacity throws
POLYCALL_E_TOO_LARGE, reports the needed size in output() and leaves the
message queued.
Arguments are checked before they reach C (PHP FFI would otherwise wrap or
truncate them silently): timeouts must be in 0..Polycall::WAIT_FOREVER
(-1 used to become "wait forever"), a recv() capacity must be >= 0
(a capacity above the 1 MiB message limit is clamped, never allocated),
handles must fit int32, and text arguments (paths, ids, endpoints, JSON,
tokens) must not contain NUL bytes. Violations throw PolycallException
with POLYCALL_E_INVALID_ARGUMENT / POLYCALL_E_INVALID_HANDLE; this
includes runConfig() with a NUL in the path. Payloads stay binary-safe.
Ownership: a Peer from openPeer() owns its node and closes it once —
on close() or when the object is destroyed. Peer::fromHandle() wraps a
handle owned elsewhere (another Polycall instance or thread) and never
closes it on destruction.
Threads: an NTS PHP has no threads, so nothing in the process can call
cancel() / close() while recv() blocks; use bounded recv()
timeouts there. On a thread-safe (ZTS) PHP with
ext-parallel, another
thread can create its own Polycall (same library, same process-wide
handles) and wake a blocked recv() with
Peer::fromHandle($pc, $handle)->cancel() or ->close(); this is tested.
Tests
tests/run-real-core.sh runs the PHPUnit suite (tests/RealCore) against
the real installed core: library/ABI checks, loader failures (missing
library, an old core without ABI v1 symbols, ABI 2), run_config, call
against a live polycall start runtime, two peer nodes exchanging payloads
both ways (empty, UTF-8, binary with NUL, exactly 1 MiB, 1 MiB + 1), registry
ownership, de-duplication, auth, dead peers, timeouts, small buffers,
handle lifecycle, concurrent senders, argument bounds, a non-ASCII config
path, and interop with a C node run by polycall peer serve / send /
recv. On a ZTS PHP with ext-parallel it also runs the threads group:
cancel / close waking a recv() blocked in another thread, threaded senders
on one handle, concurrent calls. A missing PHP, ext-ffi, PHPUnit or core is
reported as SKIP (exit 77), never as success; a skipped test fails the run.
composer install
sh tests/run-real-core.sh # core in /opt/polycall, or set POLYCALL_PREFIX / POLYCALL_LIBRARYtests/interop/exchange.php is the cross-language exchange script used with
the Ruby binding's tests/interop/exchange.rb. npm test runs the package
metadata test and then the real-core suite.
Author and license
Nnamdi Michael Okpala ([email protected]). MIT, see LICENSE.
