Skip to content

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.

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.

$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.

$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.

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.

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.

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 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 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"
}
}
Terminal window
pam mobile runtime:list
pam mobile runtime:use 8.5
pam mobile runtime:info
pam mobile runtime:update

The 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.

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.

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.

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.

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 include
PAM 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 device

Since 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.

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 every .pam.php component during the release build:

Terminal window
vendor/bin/pam-native-optimize src build/pam-cache

The 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.

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.

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.

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:

Terminal window
cargo run --release -p pam-native-engine --example benchmark -- --check

Its 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.