Platform runtime
PAM Native 0.6 adds bounded platform primitives that share the retained runtime instead of building separate execution loops. Every coded kind, state, type, priority, and opcode is an integer-backed enum.
Typed bridge IDL
Section titled “Typed bridge IDL”IdlCompiler validates sequential module, method, and field IDs and generates
fingerprinted PHP, Kotlin, Swift, and Rust contracts:
$artifacts = IdlCompiler::compile(file_get_contents('bridge.pam-idl.json'));Schemas are limited to 1 MiB, 256 modules, 256 methods per module, and 128 fields per method.
Priority scheduler and Suspense
Section titled “Priority scheduler and Suspense”$products = new AsyncResource( fn (CancellationToken $token) => $repository->products($token), key: 'products',);$products->load(TaskPriority::UserBlocking);
return Suspense::make( $products->value(), content: fn (array $items) => ProductGrid::make($items), fallback: ProductSkeleton::make(),);Immediate, user-blocking, render, normal, background, and idle tasks run in priority order. Coalescing cancels obsolete work and draining respects a frame budget.
Worklet bytecode
Section titled “Worklet bytecode”$opacity = Worklet::input() ->interpolate(0, 200, 1, 0) ->clamp(0, 1);PNW1 worklets are bounded numeric programs. They cannot call PHP, allocate
objects, access global state, or perform I/O.
Virtualization
Section titled “Virtualization”VirtualizedList, VirtualGrid, and SectionList use Rust layout and native
recycling. Heterogeneous cells, grids, inverted/horizontal layouts, stable
keys, authored or estimated sizes, bounded prefetch, initial index, and
end-reached events are supported without per-frame PHP traffic.
Background work and offline sync
Section titled “Background work and offline sync”BackgroundJobs stores idempotent work in OfflineMutationQueue. Its snapshot
can be persisted with PAM storage or Nitro and restored after process death.
$jobs->dispatch( 'messages.sync', uniqueKey: 'conversation:42', payload: ['conversationId' => 42],);The queue has typed queued, sending, applied, retry, conflict, and failed states, plus capped exponential backoff, 256 KiB payloads, and 10,000 entries.
Hardware-accelerated Canvas
Section titled “Hardware-accelerated Canvas”return Canvas::make() ->roundedRectangle(8, 8, 120, 48, 12, 0xFF6750A4) ->circle(180, 32, 24, 0xFFFFD23F) ->line(8, 80, 220, 80, 4, 0xFF111111);Android and UIKit render retained vector commands using platform-accelerated drawing. Geometry must be finite; command count and payload size are bounded.
Server-driven UI
Section titled “Server-driven UI”Server-driven documents are data, not downloadable PHP:
$tree = ServerDrivenUi::render( $document, actions: fn (string $name): ?Closure => match ($name) { 'offer.open' => $this->openOffer(...), default => null, },);Only allowlisted native nodes, numeric styles, and locally resolved actions are accepted. A document cannot name or call PHP classes or functions. Documents are limited to 1 MiB, 10,000 nodes, 64 levels, and 16 KiB text values.
PAM-owned PHP runtime
Section titled “PAM-owned PHP runtime”PAM owns, builds, checksums, and versions the PHP runtime used by a Native app.
The project declares the supported series and release channel in
pam-native.json; development, profiling, and release builds then resolve the
same runtime lock.
{ "runtime": { "php": "8.5", "channel": "stable" }}pam mobile runtime:listpam mobile runtime:use 8.5pam mobile runtime:infopam mobile runtime:updateThe resolved artifact is written to .pam-native/runtime.lock.json. Runtime
IDs have the form <php-version>-r<revision>: a PHP security release changes
the PHP version, while a PAM build or flag change increments the revision. Keep
pam-native.json in version control and preserve the generated lock as a CI
artifact. PAM Native consumes the runtime; it does not publish a separate PHP
binary.
PHP extensions on Android and iOS
Section titled “PHP extensions on Android and iOS”Android and iOS embed the same PHP build, compiled with --disable-all. What
is not in this table does not exist on the device, even when the desktop PHP
running pam dev has it.
| Extension | Android | iOS | Notes |
|---|---|---|---|
| Core, standard, date, pcre (JIT off), hash, json, random, Reflection, SPL | ✅ | ✅ | Always compiled. |
| ctype, filter, session, tokenizer, Phar | ✅ | ✅ | |
| Zend OPcache (no JIT), uri, lexbor | ✅ | ✅ | PHP 8.5 runtime. |
| mbstring | 🟡 polyfill | 🟡 polyfill | Not compiled; pushinbr/pam-native 1.17.0+ defines the mb_* functions in PHP. |
| sodium, openssl | 🟡 native | 🟡 native | Not compiled; pushinbr/pam-native 1.19.0+ covers Ed25519 verification and AES-256-GCM with Pam\Native\Crypto through the host. |
| intl | ❌ | ❌ | Not bundled: ICU adds ~35 MB per ABI. |
| iconv, zlib, curl, gd, dom/xml/simplexml, pdo/sqlite3, fileinfo, bcmath, gmp, posix, pcntl, sockets | ❌ | ❌ | Use the native modules (HTTP, database, media) instead. |
mbstring polyfill
Section titled “mbstring polyfill”Since PAM Native 1.17.0 the SDK autoloads src/Polyfill/mbstring.php
(Composer autoload.files) before any application or package code. Each
function is defined only when missing, so a real ext-mbstring (desktop, tests)
or an application polyfill loaded first wins. It follows PHP 8.5’s
ext-mbstring, including malformed input and mb_substitute_character() modes,
full Unicode case mapping (final sigma, Turkish ISO-8859-9), East Asian widths,
mb_detect_encoding() scoring and the same ValueErrors:
mb_check_encoding, mb_chr, mb_convert_case (all MB_CASE_*),
mb_convert_encoding, mb_convert_variables, mb_decode_numericentity,
mb_detect_encoding, mb_detect_order, mb_encode_numericentity,
mb_encoding_aliases, mb_get_info, mb_internal_encoding, mb_language,
mb_lcfirst, mb_list_encodings, mb_ltrim, mb_ord,
mb_preferred_mime_name, mb_rtrim, mb_scrub, mb_str_pad,
mb_str_split, mb_strcut, mb_strimwidth, mb_stripos, mb_stristr,
mb_strlen, mb_strpos, mb_strrchr, mb_strrichr, mb_strripos,
mb_strrpos, mb_strstr, mb_strtolower, mb_strtoupper, mb_strwidth,
mb_substitute_character, mb_substr, mb_substr_count, mb_trim,
mb_ucfirst.
Encodings: UTF-8, UTF-16/32 (BE/LE), UCS-2/4 (BE/LE), ASCII, 8bit and every
single-byte code page (ISO-8859-1…16, Windows-1251/1252/1254, KOI8-R/U, CP866,
CP850, ArmSCII-8). Not available: SJIS, EUC-, BIG-5, GB18030, ISO-2022-,
UTF-7, mb_ereg*/mb_split (use preg_* with u), mb_convert_kana, the
MIME header functions and mb_send_mail.
Crypto without ext-sodium and ext-openssl
Section titled “Crypto without ext-sodium and ext-openssl”Since PAM Native 1.19.0, Pam\Native\Crypto is the SDK’s crypto API. Signed
OTA manifests (Update\UpdateVerifier) and the local-first journal
(LocalFirst\EncryptedJournal) use it; before 1.19.0 both called
ext-sodium/ext-openssl directly and failed on the device.
use Pam\Native\Crypto;
Crypto::ed25519Verify($signature, $message, $publicKey); // bool, raw 64/32 bytes$sealed = Crypto::aes256GcmEncrypt($plaintext, $key, $nonce, $aad); // ciphertext . 16-byte tag$plaintext = Crypto::aes256GcmDecrypt($sealed, $key, $nonce, $aad); // ?string, null when not authentic| Runtime | Ed25519 verify | AES-256-GCM |
|---|---|---|
| Desktop, server, tests | ext-sodium | ext-openssl (or ext-sodium) |
| Android | Kotlin verifier, every API level | Platform JCA/Conscrypt |
| iOS | CryptoKit Curve25519.Signing |
CryptoKit AES.GCM |
Every backend gives the same bytes and the same accept/reject decisions as
libsodium and OpenSSL, including libsodium’s Ed25519 rules (canonical S, no
small-order R or public key, canonical public key, cofactorless check); a
shared set of libsodium/OpenSSL vectors is replayed on PHP, Android and iOS.
A journal sealed on desktop opens on a phone and back. When no backend exists
(a device host older than 1.19.0) the calls throw CryptoUnavailableException
and UpdateVerifier refuses the update instead of crashing.
hash_hmac(), hash_hkdf() and random_bytes() are always available.
Store\EncryptedStatePersistence (sodium_crypto_secretbox) still needs
ext-sodium and throws on the device.
Compiling the extensions instead would cost libsodium ~0.3–0.4 MB or OpenSSL’s libcrypto ~4.2 MB per ABI, plus a new runtime and an iOS rebuild; the platform crypto already ships on both systems.
Build-time extension audit
Section titled “Build-time extension audit”When the CLI stages an Android or iOS build it scans the application and its
Composer packages and warns about every function call or class use (new,
::, extends, implements) from an extension the selected runtime does not
compile, unless a bundled file or the polyfill declares it or the same file
guards it with extension_loaded(), function_exists() or class_exists():
PAM Native warning: src/Money.php:14: class NumberFormatter comes from ext-intl, which the PAM mobile PHP runtime (Android/iOS) does not includePAM Native warning: src/Ota.php:9: function sodium_crypto_sign_verify_detached() comes from ext-sodium, which the PAM mobile PHP runtime (Android/iOS) does not include; use Pam\Native\Crypto::ed25519Verify(), which works on the deviceSince 1.19.0 the SDK is audited like any other package (its own sources produce
no warning) and a warning names the device-ready replacement when there is one
(Pam\Native\Crypto, random_bytes(), bin2hex(), base64_encode()…).
Warnings never fail the build.
Development, production, and benchmark modes
Section titled “Development, production, and benchmark modes”Set PAM_NATIVE_MODE before starting or building the application:
| Value | Behavior |
|---|---|
development |
Runtime diagnostics and flexible compiler checks. |
production |
Strict compilation with profiling removed from the hot path. |
benchmark |
Strict compilation with performance measurements enabled. |
PAM_NATIVE_STRICT=1 enables production compiler checks during development.
Strict compilation rejects dynamic component properties and runtime code
evaluation that would prevent deterministic dependency analysis.
Why rendering remains bounded
Section titled “Why rendering remains bounded”PAM treats performance as a set of cooperating contracts:
- binary bridge batches, pooled ABI buffers, and append-only typed IDL avoid per-command JSON and recycle native allocations;
- retained node identity applies property and structure patches only where the tree changed;
- the scheduler uses the physical 60/90/120 Hz refresh rate as its deadline and protects input work through priority, cancellation, and coalescing;
- virtualized lists use stable keys, variable extents, velocity-biased overscan, and logarithmic window lookup to keep 100,000-item data sets bounded;
- every queue and cache has a limit, and platform memory-pressure signals can reclaim retained resources.
Component-local state and Store reads are recorded against the component being rendered. A write invalidates only its subscribers, so clean components keep their previous element subtree and descendant lifecycle state. Components that use untracked mutable public fields retain full-render behavior for compatibility.
Incremental layout marks changed node IDs and their ancestor paths dirty. The Rust engine recalculates the affected container, prunes clean subtrees with the same retained frame, and removes stale geometry when visibility changes. Fixed-size text boxes can skip intrinsic measurement when their resolved geometry cannot change.
Compile the production fast path
Section titled “Compile the production fast path”Compile every .pam.php component during the release build:
vendor/bin/pam-native-optimize src build/pam-cacheThe optimizer emits versioned component metadata, a SHA-256 manifest, and a
relocatable pam-preload.php. Loading the preload primes OPcache and generates
constructor factories and prop schemas before the first application frame, so
template parsing and component reflection leave the startup hot path. Cache
format versions invalidate stale metadata safely.
Profiling and the performance observatory
Section titled “Profiling and the performance observatory”The bounded Profiler retains at most 512 spans for php.render, php.encode,
and scheduler.task, including duration, timestamp, and integer priority.
Android adds decode/mount trace sections, frame metrics, startup
macrobenchmarks, and Baseline Profile journeys; iOS exposes renderer metrics to
the runtime callback and Instruments.
The Rust observatory reports frame deadline misses, retained bytes, buffer reuse, node counts, coalesced commands, and P95 decode, reconcile, layout, and encode latency through the stable C ABI. Both DevTools overlays consume those same counters.
Recovery and safe mode
Section titled “Recovery and safe mode”After a confirmed native commit, the runtime publishes a throttled atomic
checkpoint. A rejected commit never replaces lastFrame, preserving the last
known-good native hierarchy. Stable failure fingerprints track consecutive
errors; three consecutive failures enter safe mode instead of creating a
restart loop.
When an incremental patch is invalid, the encoder preserves PHP component identity and cached subtrees, discards only the previous wire snapshot, and immediately emits one complete tree. This recoverable path does not open an error overlay. If the complete tree is also rejected, the error remains visible and the checkpoint does not advance.
Release gates
Section titled “Release gates”The Native CI contract covers PHP SDK and lifecycle tests, deterministic 1,000-frame tree fuzzing, a 1,001-node encoder budget, Rust release-mode gates, Android API 26/36 instrumentation and macrobenchmarks, iOS simulator coverage for a bounded 100,000-item window, and optimized release builds.
Local thresholds can be overridden with PAM_PERF_FIRST_FRAME_MS and
PAM_PERF_STEADY_FRAME_MS. Reproduce the native engine gate with:
cargo run --release -p pam-native-engine --example benchmark -- --checkIts full-tree and patch ceilings use PAM_PERF_ENGINE_FULL_TREE_NS and
PAM_PERF_ENGINE_PATCH_NS. Device macrobenchmarks remain the source of truth
for startup, frame timing, and memory.