Skip to content

Releases: m3m0r7/pnl

v0.7.0

Choose a tag to compare

@m3m0r7 m3m0r7 released this 28 Jun 06:12

A large step toward 1.0: a holistic schema redesign, a full layered refactor, new runtime/tooling features, and several codegen correctness fixes surfaced by a fresh Alpine Linux sweep.

⚠️ Breaking changes (no backward-compat — still beta)

The pnlx.json / pnl.json schema was redesigned so each key's meaning is clear, separating compile-time options from runtime options. JSON Schemas are frozen and date-versioned under schemas/<name>/2026-07-01/.

  • pnl.json features (runtime): use_functionsglobal_functions, allow_cdatacdata_arguments, use_php_scalars_in_params/return/constscalar_params/scalar_returns/scalar_constants.
  • pnl.json config: self_repository/packages_repositoryrelease_repository/package_repository; load_pathslibrary_paths.
  • pnlx.json: requiresnative_libraries; new compile_options{headers, definitions} (was top-level headers + require_definitions); new setup{build_script, install} (was self_build + installation); installation.<os>.installsetup.install.<os>.commands; checkIfExistscheck_if_exists.
  • lockfile / pathmap: requiresnative_libraries.
  • CLI flags renamed to match: --enable-global-functions, --enable-cdata-arguments, --enable-scalar-returns / --enable-scalar-constants.

Features

  • pnl doctor — diagnoses the toolchain: libclang (required), a C compiler, pkg-config, PHP + the FFI extension, and the workspace; exits non-zero on a required-check failure.
  • ftps native-library fetching — via suppaftp with native-tls (Security.framework on macOS, schannel on Windows, vendored OpenSSL on Linux, shared with git2).
  • Pnlx\FFI\Allocator + AllocationScope — allocate inside the extension's own FFI scope and pin allocations against premature GC (the common use-after-free), with a use-after-release guard.
  • Constraint-aware dependency resolution — a version a dependency pins that conflicts with an already-installed version is now reported, not silently mis-resolved.

Codegen correctness fixes

All four were surfaced by a fresh Alpine Linux (musl) docker sweep and verified there:

  • Case-colliding type files. A C struct tag and its typedef differing only in case (e.g. Argon2_Context / argon2_context) generated two PHP files declaring the same (case-insensitive) class — fatal on a case-sensitive filesystem, while on macOS the field-less wrapper silently won. They are now deduplicated, keeping the field-carrying wrapper.
  • Quoted-include sibling headers. A library whose API lives in headers included by the entry header (z3's z3_api.h, hdf5's H5public.h) was generating a silently empty binding. Headers pulled in via #include "..." are now treated as package-owned.
  • Typedef names eaten by macro neutralization. An all-uppercase typedef (gdbm's GDBM_FILE) matched the ABI-attribute-macro heuristic and was stubbed empty, truncating the pointer to int (a segfault at call time). Typedef names are now excluded from neutralization.
  • Struct-by-value arguments and returns. Structs passed or returned by value (quickjs's JSValue, gdbm's datum) were marshalled as pointers; the generated marshaller now passes and receives them by value.

Refactor & internals

  • Full layered module reorganization: util ← model ← {native, sources} ← codegen ← app (plus embed), with the largest god-files split apart.
  • Lints codified in Cargo.toml (-D warnings, clippy::all).
  • New conventions document at docs/{en,ja}/conventions.md.

Verification

Alpine 3.21 docker sweep across all packages: 172/180 install, 171/172 installed-package examples pass. The remainder are packages genuinely absent from Alpine repos, plus one macOS-only exported symbol (p11_kit_check_version).

v0.6.0

Choose a tag to compare

@m3m0r7 m3m0r7 released this 22 Jun 13:45

Features

  • Compiled static-inline shims (compile_options.static_inline). A C static inline function is defined in a header and exports no symbol, so PHP FFI cannot bind it — by default the generated method throws. Opt in (consumer-side, in your pnl.json) and pnl install now emits a tiny extern "C" trampoline for each such function, compiles it to a small shared library next to the package, and co-loads it, so the function becomes an ordinary callable method. pnl never reimplements C; the real compiler does the work.
    • A C compiler is discovered at install time ($CC, then cc/clang/gcc, honouring $CFLAGS/$LDFLAGS). The C compile is retried as C++ when it fails, so C-API libraries whose headers only compile as C++ (e.g. assimp) are handled.
    • Opt-in, with graceful degradation. A missing compiler errors only when a package actually has static inline functions to shim; a shim that fails to compile falls back to throwing stubs (the install still succeeds). Default installs continue to require only libclang.
  • pnl config <key> [value]. A git-config-style getter/setter for pnl.json, schema-validated (pnl config compile_options.static_inline true, --unset to reset). After a change it offers to reinstall so generated output reflects it. pnl install --enable-static-inline is the per-install equivalent.
  • wchar_t * wide strings. A wchar_t * struct field (e.g. hidapi's manufacturer_string/product_string/serial_number) now generates a ?string getter that decodes the wide string (UTF-32 → UTF-8) via Util::wcStringOrNull, instead of exposing an int pointer.

Notes

  • An integrity-digest mismatch now prompts before overwriting when run interactively (default: no), instead of only aborting.
  • Verified across the 181-package corpus: 16 libraries build shims (libnotcurses 215 functions, libvips 90, libjasper 86, libpango 72, libhwloc 59, …) with no regressions; the remaining hard cases (transitive includes libclang itself drops, or C++ typeof macros) fall back to stubs and still install. Documentation (EN + JA) updated.

v0.5.5

Choose a tag to compare

@m3m0r7 m3m0r7 released this 22 Jun 07:09

Fixes

  • Global functions now declare enum return types. A generated global helper
    (features.use_functions) for a function returning a C enum declared its return
    as int|<wrapper>, but the entity it delegates to returns the generated PHP enum —
    so calling it (e.g. \Pnlx\Func\Libsdl\SDL_HasEvent(), which returns SDL_bool)
    threw a TypeError. The global function now returns <Enum>|null, matching the
    entity method.

v0.5.4

Choose a tag to compare

@m3m0r7 m3m0r7 released this 22 Jun 05:57

Extension composition, reworked

This release matures the runtime composition introduced in v0.5.3 into a trait +
attribute model and adds a CLI for it.

Entity model: pure C-function surface

Generated entities now carry only the methods named after C functions. The
method group lives in a generated <Class>LibraryComponent trait the entity mixes
in; all runtime machinery (booting, the loaded library, dispatch) moved to
Pnlx\FFI\NativeLibraryRegistry, and install-time metadata moved from constants to
a #[\Pnlx\Attribute\NativeLibrary] attribute. The only inherited surface is the two
magic methods (__construct, __callStatic), which a C function can never be named —
so composing several Components can only ever clash on real C-function names.

Compose into one shared FFI scope

A composed class mixes in several Components and shares ONE FFI scope, so a CData
returned by one library (e.g. an SDL2_image surface from IMG_Load) flows straight
into another (SDL_CreateTextureFromSurface):

  • Pnlx\Runtime::compose([A::class, B::class]) — runtime: returns a class with
    real methods, so by-reference out-parameters round-trip ($x->SDL_QueryTexture($t, $w, $h)).
  • pnl compose <a> <b> --as <Class> [--prefix <p>] — generates a NAMED composite
    class file (for editor/static-analysis support), recorded in pnl.json composites
    and wired into the autoload. --prefix resolves method-name collisions via trait
    insteadof/as. You can also hand-write new class extends AbstractExtension { use ACompoent; use BComponent; } — composition needs no special runtime call.

Boot shares the one scope with each member entity, so a pointer wrapped/allocated via
a member's type class is usable by the composite.

use_functions

With features.use_functions, pnl compose also emits composite global functions
under \Pnlx\Func\<Class>\* that delegate to the composite class (sharing its scope;
by-reference out-parameters preserved), retargeted from each member's own
functions.php with --prefix applied to collisions.

Other

  • New composites section in pnl.json (schema pnl/2026-07-01).
  • build.rs now fingerprints schemas/ (as it already did src/sdk) so a schema
    edit always re-embeds into the support library — the runtime no longer validates
    against a stale schema.

NOTE: composing two libraries that export the same C symbol shares one global
symbol (the first loaded wins); pnl compose --prefix de-conflicts the PHP names but
warns about this.

v0.5.3

Choose a tag to compare

@m3m0r7 m3m0r7 released this 22 Jun 02:07

Runtime extension composition

Pnlx\Runtime::compose([Libsdl::class, Sdlimage::class]) fuses several generated
extensions into one shared FFI scope, so a \FFI\CData returned by one package
(e.g. an SDL2_image surface from IMG_Load) can be passed straight into another
package's function (SDL_CreateTextureFromSurface). Previously each extension
booted its own FFI::cdef scope and PHP FFI rejected cross-scope CData.

  • Members' generated cdefs are merged (Pnlx\FFI\CdefComposer) and their libraries
    co-loaded into a single NativeLibrary (NativeLibrary::composite()); each member
    class adopts that scope (AbstractExtension::pnlxAdoptNativeLibrary()), so its
    generated methods keep marshalling and return-wrapping unchanged — only the native
    scope is shared.
  • Returns a Pnlx\ComposedScope that also proxies calls
    (Runtime::compose([A, B])->some_function(...)); plain A::fn() / B::fn() static
    calls share the scope too.

Code generation

  • Pointer-to-const scalars (const int *, const double *) are now treated as
    read-only input arrays instead of writable by-reference out-params — fixes passing
    null where PHP cannot pass a literal by reference (libopenal/libswscale).
  • Structs defined with a body always get a Types\* wrapper now, even when no
    function names them, so a struct handed back through void * (hiredis's
    redisReply) can be re-wrapped.
  • Generated enums gain toInt() (alias of ->value), mirroring the integer-wrapper
    API of scalar returns.
  • Typed wrappers reinterpret() an incoming \FFI\CData to their own type.

Native-library resolution

  • pkg-config lookup also tries a lib-prefixed module name (enet -> libenet).
  • Headers living under include/<pkg>/ add the include root to the parse path, so
    angle includes carrying the subdir prefix resolve (lzo's <lzo/lzodefs.h>).

Build / runtime

  • The embedded PHP SDK (include_dir!) is rebuilt whenever any src/sdk file
    changes, via a build-script content fingerprint — SDK fixes always ship in the
    binary instead of silently going stale.
  • Pnlx\Util::isNull() added as a static null-pointer check for generated code.

v0.5.2

Choose a tag to compare

@github-actions github-actions released this 21 Jun 23:54

Release v0.5.2

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 21 Jun 05:32

Highlights

Missing libclang now fails fast — before any download — with an actionable message.

libclang is pnl's one hard external requirement for reading C headers, and the
readiness check (pnl -i) plus the platform-specific "how to install it" message
already existed. But during pnl install the check only fired deep inside binding
generation — after the package archive was downloaded, dependencies were installed
recursively, and native libraries and their headers were resolved and fetched. On a
fresh machine without the toolchain you paid for all of that before being told to run
xcode-select --install.

The check now runs up front, so you get the message immediately instead of at the end.

What changed

  • pnl install preflights libclang right after reading the package manifest —
    before dependency recursion and native-header downloads.
  • pnlx gen preflights right after resolving the library key, before headers are
    read.
  • The message is unchanged and stays platform-specific:
    • macOSxcode-select --install (the Command Line Tools bundle libclang),
      plus a LIBCLANG_PATH hint if they're already installed.
    • Linux — the distro package to install (apt install libclang-dev,
      dnf install clang-devel, apk add clang-dev, pacman -S clang).

No false positives

The preflight is gated on the effective symbol prefix, so the curated
verbatim-header path (libc and friends, which emit a cdef without libclang) is
never blocked — those packages install with no toolchain at all, exactly as before.


Full diff: v0.5.0...v0.5.1

v0.5.0

Choose a tag to compare

@m3m0r7 m3m0r7 released this 21 Jun 04:46

Highlights

A type-layer and resolution release: generated PHP now models C structs and
enums as first-class typed values, native-library resolution gained .tbd and
linker-script handling, and the toolchain footprint shrank to libclang only.

Structs & enums in the generated API

  • Struct accessors. Each generated Types\<tag> wrapper exposes typed
    getField()/setField() accessors. Structs that embed another struct/union by
    value
    now render too (decided by a render-time fixpoint over the genuinely
    emittable aggregates), so a struct passed or returned by value works through
    PHP FFI — quickjs's JSValue round-trips by value end to end.
  • Enums. A C enum becomes an int-backed PHP enum. Parameters accept
    Enum|int; a return is mapped back with Enum::tryFrom() (Enum|null).

Fewer external tools — libclang is the only requirement

  • The export-symbol filter parses ELF / Mach-O / PE directly (the object
    crate) instead of shelling out to nm. This fixes a real failure on hosts without
    binutils, where unexported declarations (SDL's app-provided SDL_main) leaked into
    the cdef and broke FFI::cdef.
  • Local git-origin lookup goes through libgit2; the only pkg-config use (a test) is
    gone. No nm, git, or pkg-config binary is needed at install time.

Native-library resolution

  • Ordered fallback chain. library_names is tried in order; a virtual entry
    resolves to a real on-disk file when one exists (so the export filter can run) and
    only falls back to a bare-name load when none does.
  • macOS .tbd support. A new load_type (auto|elf|tbd|dll|dylib) plus a .tbd
    parser lets macOS system libraries resolve through the SDK text stub for their
    exports while the runtime loads the dylib by its install-name from the dyld
    shared cache. The active SDK is found via xcrun/xcode-select.
  • Linker scripts / split libraries. A GNU ld linker script
    (libncurses.soINPUT(libncurses.so.6 -ltinfo)) is parsed and its inputs are
    co-loaded, so a symbol split into a sibling .so (curses_version in libtinfo)
    resolves; a linker-script "exact match" is skipped in favour of the real soname.

Generator robustness

  • A qualifier macro (#define RESTRICT restrict) no longer leaks into a parameter.
  • An enum's own forward typedef no longer self-references when projected.
  • An empty-symbol_prefix (verbatim header) package still drops declarations the
    library does not export (glibc's atexit, in libc_nonshared.a).

Tooling

  • tests/docker-sweep/ — a committed, build-once, single-foreground-container,
    internally-parallel pnl install + EXAMPLES.md sweep across Alpine and Ubuntu,
    with agent rules for interpreting results.

Validation

Swept all packages without binutils on Alpine (musl) and Ubuntu (glibc): every
installable, non-hardware package's example runs, with zero cdef load-time errors
on both. The only remaining example failures are hardware-gated (an HID device, a
serial port). Rust (162) and PHP (65) suites green; clippy, php-cs-fixer, phpstan and
schema checks clean.

v0.4.3

Choose a tag to compare

@m3m0r7 m3m0r7 released this 20 Jun 02:09

Highlights

Callbacks: pass a PHP callable where a C function pointer is expected.

Until now every function-pointer parameter was generated as an opaque void *, so callback-taking C APIs (comparators, write/visit callbacks, and similar) couldn't actually be driven from PHP — PHP FFI rejects a closure for a void *. They are now generated as real C callback types, so a PHP closure or named callable is passed straight through and PHP FFI builds the C callback trampoline.

What you can do now

// int example_apply(int value, int (*callback)(int));
$result = Example::example_apply(20, static fn (int $v): int => $v * 2); // 41
  • Generated wrappers type a function-pointer parameter as callable (the cdata variant also accepts \FFI\CData).
  • The callback's arguments and return value are marshalled from the C signature in the cdef — an int arrives as a PHP int, a pointer arrives as an \FFI\CData.
  • Both a closure and a plain callable (a function name) are accepted.

Faithful, typed callback signatures

Each component of the callback keeps its real, declared type, so your closure receives something useful:

  • A pointer to a type the package already emits (its own struct/typedef) stays that type — e.g. int (*cb)(const struct point *) — so the closure gets a typed \FFI\CData and can read fields ($p->x) without a cast.
  • A const char * callback argument stays a char *, readable with \FFI::string().
  • Scalars keep their exact C width.
  • A component degrades to void * only when its spelling can't be placed in a function-pointer declarator (a nested function pointer, an array, or an anonymous aggregate); the whole parameter falls back to void * for a pointer-to-function-pointer or a by-value aggregate. A type referenced only through a callback signature is backfilled, so the cdef still loads.

Scope & caveats (PHP FFI itself)

  • Synchronous callbacks — invoked while the C call is on the stack (the common case) — are fully supported.
  • A stored / asynchronous callback (kept by C and called after the call returns) inherits PHP FFI's own trampoline-lifetime limitation and is not guaranteed safe.
  • Throwing out of a callback is fatal (Throwing from FFI callbacks is not allowed) — handle errors inside the callback and return a value.

Validation

New unit coverage for callback rendering and the backfill extractor, plus an end-to-end test that invokes both a closure and a named callable through the compiled native fixture. Generated golden snapshots updated. Rust (154) and PHP (62) suites green; clippy, php-cs-fixer, and phpstan clean.

v0.4.2

Choose a tag to compare

@m3m0r7 m3m0r7 released this 19 Jun 12:52

Highlights

v0.4.2 makes raw PHP scalars the default for generated method arguments, so out of the box you can pass plain int/float/string without wrapping them in \Pnlx\Types\*. It also ships the in-repo agent skills documentation under .agents/.

use_php_scalars_in_params on by default

  • features.use_php_scalars_in_params now defaults to true: generated methods accept a raw PHP scalar argument directly, instead of requiring a \Pnlx\Types\* wrapper.
  • Applied consistently across every layer that resolves the flag:
    • Rust manifest serde default (#[serde(default = "default_true")]) and the PnlFeatures Default impl.
    • PHP Pnlx\Runtime::useScalarsInParams() falls back to true when the key is absent from pnl.json, so hand-written manifests that omit the field behave the same as generated ones.
    • JSON schema (schemas/pnl/2026-07-01/schema.json) now records "default": true.
  • To keep the stricter wrapper-only behavior, set features.use_php_scalars_in_params to false in pnl.json.

Removed the redundant install flag

  • Dropped pnl install --enable-use-php-scalars-in-params and its InstallOptions plumbing — the feature is on by default, so opting in is no longer needed. Opting out is done by editing pnl.json.

Agent skills documentation

  • Added .agents/skills/ guidance: pnl-usage, pnl-php-sdk, and pnlx-authoring SKILL.md files (surfaced to Claude Code via the .claude/skills symlink).

Documentation

  • Updated docs/en/configuration.md and docs/ja/configuration.md to describe true as the default for features.use_php_scalars_in_params.

Validation

  • Rust: cargo build, cargo clippy, cargo fmt --check, and cargo test (152 passed) all clean.
  • PHP: PHPUnit (61 tests, 276 assertions) and PHPStan level max pass.