Conversation
This was referenced Aug 19, 2026
The EasyEntry Send comment overclaimed that the table mutex prevents two threads from ever driving the same handle. It doesn't: perform/free both drop the lock before calling into libcurl (to avoid a write-callback deadlock), so a concurrent free(id) can free the CURL* out from under an in-flight perform(id) on the same id. Document the actual guarantee (table integrity only) and the caller contract that makes today's usage sound (no concurrent calls on the same id), at both the Send impl and the perform/free ABI entry points, so a future concurrent caller (e.g. Task 9) doesn't rely on the old, inaccurate wording.
… param The example's headline request used https://example.com/, which this landing's curl/openssl recipe cannot reliably verify (no CA bundle baked in) - swap it for plain HTTP and note HTTPS arrives with Task 8's TLS wave. Verified end to end against the real network via a scratch native-project link (real HTTP 200, real connection-refused failure). curl_close()'s parameter was renamed $handle -> $_handle to silence an unused- variable warning, but the name is PHP-visible (curl_close(handle: $ch) is legal named-argument syntax) and the rename silently broke it. Restored $handle verbatim and documented the accepted warning instead of inventing a new suppression mechanism.
`--enforce-target-architecture` matched only single-expression match arms, so the two arms rustfmt wraps in braces (CurlSetoptUnsupportedWarning and CurlMultiSetoptUnsupportedWarning, whose symbol names do not fit on one line) were invisible to the name map. The audit reported four errors and exited 1 — which the builtins-docs-sync CI job runs as a step. Pre-existing on this branch: the errors go to stderr, so a merged `| tail` hides them behind the summary. Applied the same tolerance to the UnaryStringRuntime map, which has the identical latent hole.
The generated builtin documentation is now produced in ONE canonical configuration: `cargo build --example gen_builtins --features curl`. That is the only configuration in which the exporter can see both halves of ext/curl — the shared catalog's PHP-visible `curl_*` contracts and Magician's matching eval bindings — so the previous default-feature build was structurally unable to document 34 functions the compiler fully supports. - extract.py refuses to run against a default-feature exporter instead of silently emitting a catalog 34 rows short, and routes prelude entries to the prelude that actually declares them (curl_prelude.rs, not hash_prelude.rs). - render.py names that prelude from the extracted lowering rather than hard-coding "hash". - registry.py gains curated curl param/return types transcribed from the prelude's own PHP declarations, on the hash_* precedent; curl_version() and curl_multi_info_read() stay `mixed` because the prelude declares no return type for either. - audit_builtins.py pins the 47 non-registry routes and audits every curl row for AOT-through-prelude plus an eval registry binding. - gen_php_comparison.py gains NEWER_THAN_BASELINE for PHP functions that postdate the vendored 8.4 snapshot, so the two PHP 8.5 curl additions are reported rather than miscounted. The curl coverage row goes 0/33 -> 32/33. Regenerated: 621 registry rows (587 + 34), 1160 pages. Idempotent.
…cords The hash_init fixture was a hand-built dict with no lowering block, so it exercised the renderer's prelude-neutral fallback rather than the naming path. Feed both prelude families their committed registry records instead, and assert curl pages name the curl prelude.
…re on curl-feature-contract gains the two root-level suites the new relay makes runnable: the cross-backend parity integration test and the compiler's prelude-signature parity lib test. Neither needs a native libcurl — they read builtin metadata and never reach Magician's eval dispatch table, so nothing references elephc_curl_* — which keeps the job cheap and always-on. builtins-docs-sync now builds the exporter with --features curl, so the drift gate runs in exactly the configuration that produced the committed pages. Both jobs were already in the test umbrella's needs and assertions; no new job. actionlint: 4 findings, same 4 as the HEAD baseline.
The catalog_curl module doc, elephc-magician's feature header, the punch-list ledger and the ROADMAP all described a world where no configuration could see the curl surface: no parity coverage, no generated docs, only the hand-written docs/php/curl.md. The root relay and the feature-on docs configuration make all of that stale. docs/php/curl.md now links the generated per-function pages.
The leading-character check allowed a space before `function <name>(`, so a prose mention on a comment line could have been picked up as the declaration (and `find` takes the first match). Require the match to be preceded by nothing but whitespace on its own line.
…t the inverse The prelude parity test recorded only whether a parameter had a default, never which one, and never read the declared PHP type. Compiled code executes the default the prelude writes and eval() executes the catalog's, so a prelude `float $timeout = 5.0` against a catalog Float(1.0) left every registry, both coverage audits and the generated docs green while curl_multi_select($mh) waited five seconds in one backend and one in the other. The catalog side has the committed registry JSON as a review backstop; the prelude side had none. Compare the default's value (floats and ints parsed, not string-matched) and the declared type (compatibility, since TypeSpec has no object/array vocabulary — but a Mixed contract may not be narrowed to a scalar in the prelude). Handle PHP variadics explicitly: `...$rest` carries no `=` and would otherwise read as a missing default. Negative-controlled on a drifted default and a drifted type, both reverted. Add the inverse audit: a PHP function a catalog-hosted prelude declares without a contract is invisible to both parity suites and to the generated docs, since all of them iterate contracts(). curl_file_create is the one instance and is now permanently excluded by a named allowlist entry carrying its rationale, checked in both directions. Scoped to the hash and curl preludes, and to configurations whose catalog slice is published — the other five preludes ship PHP surfaces with no shared contracts at all by design. Restate what the integration suite's eval signature comparison proves: eval_builtin! submits only a contract ID and EvalBuiltinSpec::from_binding derives the whole signature from eval_signature(contract), so assert_signature_shape compares the contract with itself. That is main's single-source-of-truth architecture making eval signature drift unrepresentable, not a hole — but it is an existence check, and the doc comment said drift detection. Also compare the curl by-reference set as a BTreeSet so a catalog reorder cannot fail for a non-reason.
…rface Two silent fallbacks in the docs extractor. An unmapped contract area fell through to hash_prelude.rs and a missing declaration fell through to line 1, so a prelude-provided contract added in a third area would have shipped a generated, committed page naming the wrong prelude and linking to an unrelated line. Both now raise, and the hash prelude gets its own explicit area entry instead of being the accidental default. The canonical-configuration guard also diagnosed every count mismatch as a stale default-feature binary. Only zero-found means that; any other count means the surface changed size and the pinned constant needs bumping, which is now what the message says.
The ROADMAP line read as if the cross-backend suite detected catalog-vs-eval signature drift. It cannot: Magician derives its signature from the contract, so that drift is unrepresentable rather than detected. Name what the suite does prove, and credit the hand-written signature that genuinely is audited — the prelude's PHP declaration, now including declared types and default values — plus the new inverse audit.
nghttp2 failed to build on all three CI platforms: make ran the autotools regeneration chain (am--refresh -> aclocal-1.16) and died with Error 127 on runners that have no automake. Root cause is the staging layer, not the recipe. extract_tar_gz never applied the tar header's mtime, so each file inherited the instant it was written and the relative age of any two files became a function of their position in the archive. Upstream ships aclocal.m4 deliberately NEWER than configure.ac and m4/*.m4 so the regeneration rules stay dormant (measured on nghttp2 1.70.0: 14:29:47 vs 14:29:37), but aclocal.m4 sits at tar entry 74 and configure.ac at 576, m4/* at 1581+ — so extraction inverted exactly the relationship upstream set up. Every extracted regular file now carries one fixed past instant. GNU make rebuilds only on a STRICTLY newer prerequisite, so an all-equal tree cannot trigger any regeneration rule in any package — this closes the class, not the instance, and needs no recipe revision bump: mtimes enter neither ArtifactKey nor the receipt (which hashes size + content), so no lockfile churn either. A fixed past value rather than now() also keeps two extractions of the same verified tarball identical, and keeps configure's generated output strictly newer than its sources. Why this never showed locally: Apple ships GNU Make 3.81, whose 1-second timestamp granularity collapses a fast extraction into a single second. The failure is therefore timing-dependent, not deterministic. Reproduced by restoring the inverted ordering with a >1s spread — Error 127 on aclocal.m4, identical to CI — and cleared by equalizing them. curl and libssh2 carry AM_MAINTAINER_MODE and were immune; verified by building both from inverted-mtime trees with no automake present, not by reading the grep.
Second CI red wave on the curl shards: every codegen::curl::eval::* fixture failed with an offline crates.io resolution error, then every other fixture sharing that process died with a poisoned-lock PoisonError -- including pure-AOT fixtures unrelated to eval. Root cause 1: libelephc_magician_curl.a (the curl-aware magician archive eval()+curl fixtures need) was never prebuilt or archived like every other bridge, so ensure_magician_curl_staticlib always fell through to an on-demand `cargo build --features curl`, which cannot work in a shard job running from an extracted nextest archive with CARGO_NET_OFFLINE=true and no cargo registry cache. Build it in the build-archive-<platform> jobs (same isolated --target-dir recipe the harness itself used to use on demand), ship it via a new [[profile.ci.archive.include]] entry, and make ensure_magician_curl_staticlib trust the prebuilt copy (and fail loud-and-fast if it's somehow still missing) instead of ever attempting the doomed on-demand build when ELEPHC_TEST_PREBUILT_BRIDGES is set. Root cause 2: BRIDGE_STATICLIB_BUILD_LOCK's poisoning made one fixture's build failure crash every other fixture that later locked it, regardless of whether their own bridges needed a build at all. Recover from poisoning the same way elephc-curl/elephc-pdo/elephc-image already do for FFI locks (F-QUAL-02): the guarded payload is (), so there is no shared state a panic could leave inconsistent, and recovering is always safe. The original failure stays its own loud, attributed test failure; it just stops cascading into unrelated ones. Reproduced both defects locally (direct binary invocation, offline env, empty scratch CARGO_HOME) before fixing, and verified both fixes the same way, including a negative control for the lock recovery.
The codegen harness turned each managed native package into a -l NAME and
guarded it with the same dedupe set every other named input shares. Every
streams:: fixture calls fopen($path, ...) with a non-literal filename, whose
requirements include SystemLibrary("z") (builtins/requirements.rs), so -lz was
already planned before the curl block -- and the dedupe then dropped the managed
zlib entirely. Nothing after libssh2.a could resolve the deflateInit_/deflate/
deflateEnd that libssh2.a(comp.o) needs for SSH channel compression.
That failed only on Linux because GNU ld scans archives once, left to right,
while Apple's ld64 re-scans to a fixed point: the plan was wrong everywhere and
observable in one place. Hence 49/52 green on Linux with only streams:: red.
Fixed by making the harness do what the production planner already does --
LinkItem::managed_archive with exact paths. Managed archives then never enter
the named dedupe set, so no unrelated requirement can suppress or reorder them,
and catalog order is preserved by construction. It also closes a quieter hazard
in the same code: a -lssl resolved through the search path could have bound a
system OpenSSL into fixtures whose whole purpose is proving the pinned build.
Production was NOT affected -- managed packages have always been exact paths
there -- but the property was only ever an accident of the implementation, so it
is now pinned by a test as well. Both tests negative-controlled: reverting the
harness to -l names fails all three new order tests, and mutating the production
planner to dedupe managed archives drops libz.a exactly as CI reported.
…dary `__rt_array_free_deep` reserved 24 bytes for its three spill slots after `push rbp` had already put rsp on a 16-byte boundary, so every `call` in its body ran 8 bytes off and handed the callee a stack System V x86_64 forbids. That was invisible while the callees were all hand-written assembly touching only integer registers. curl is the first thing that carries the misalignment into C: a CurlHandle released as an ARRAY ELEMENT walks __rt_array_free_deep -> __rt_decref_any -> __rt_mixed_free_deep -> __rt_curl_easy_free -> the elephc_curl bridge, and libcurl faults on the first aligned SSE spill onto that stack. Measured on linux-x86_64: both `elephc_curl_easy_free` entries arrive at rsp % 16 == 0 before this change and at the required 8 after it. That is the SIGSEGV in codegen::curl::multi::multi_get_handles_reports_add_order_and_tracks_removal, and only there: it is the one AOT fixture whose handles are freed out of arrays ($handles/$left) rather than out of a local or a property. aarch64 was never affected — its emitter already reserved 32. Pinned by the_deep_free_chain_keeps_sysv_call_alignment_on_x86_64, which generates the linux-x86_64 runtime on any host and asserts every call on the deep-free chain lands on a 16-byte boundary.
…he rule The previous commit fixed __rt_array_free_deep, the one helper of this class that curl actually reached. Seven more carried the identical signature — a `push rbp` followed by a `sub rsp` that is not a 16-byte multiple, so every `call` below hands its callee a stack System V forbids: __rt_array_chunk 40->48 __rt_range 40->48 __rt_array_chunk_refcounted __rt_json_encode_array_int 40->48 __rt_array_pad_refcounted __rt_spl_fixed_new 24->32 56->64 __rt_zval_pack_element 56->64 x86_64 arms only; spill-slot offsets and aarch64 untouched. MEASURED, NOT ASSUMED: these were latent, not live. A probe running array_chunk/array_pad/range over two live CurlHandles on linux-x86_64 reports every bridge entry aligned even with the frames still wrong, because __rt_array_push_refcounted's subtree (ensure_unique -> clone_shallow, incref, push_int -> grow) contains no decref at all and so cannot reach __rt_mixed_free_deep's resource ladder. Fixed anyway: an ABI violation one refactor away from mattering is not worth leaving in place. The rule is now enforced globally rather than helper by helper. runtime/sysv_call_alignment.rs renders the linux-x86_64 runtime for both feature profiles, walks every helper's CFG tracking rsp, and fails on any misaligned `call`. Helpers that still violate are named WITH their reason and the list shrinks only; helpers the walk cannot model are named as holes, and one that is neither fails the build. The eleven helpers a CurlHandle travels through into the bridge may never be allowlisted at all. Building the audit found what a narrower test would not: sections are not functions (the emitters lay private subroutines behind the exported helper, so nine __rt_date_* bodies and __rt_bcmath_binary were never being walked), which in turn exposed two more real violations in __rt_date and __rt_strtotime. Of the 20 allowlisted helpers, three reach code this runtime did not write and are the ones worth fixing next: __rt_usort and __rt_array_udiff_uintersect `call r12` into the user's compiled-PHP comparator, and __rt_fiber_entry calls libc setjmp. The last is deliberately left alone — coroutine entry, frame load-bearing for __rt_fiber_switch's stack hand-off, needs fiber tests first.
…output
The eval process builtins ended in
`.output().map(|o| o.stdout).unwrap_or_default()`, so a process that could
not be created AT ALL produced the same empty byte string as a command
that ran and printed nothing. exec() then answered "" for a command it
never executed.
That is the curl-feature-contract CI failure. On a loaded runner
posix_spawn fails with EAGAIN and one exec() in an otherwise-passing test
silently returned "" while its siblings succeeded. Measured directly: 2560
concurrent `/bin/sh -c "printf spread"` spawns under `ulimit -u 900`
produced 1177 EAGAIN failures, every one of which the old code reported as
"printed nothing".
The reported call shape is not implicated. exec-direct and
exec-through-call_user_func_array converge on the same
eval_process_command_result -> eval_shell_command_output after argument
binding, so nothing on the call-array path can make them differ; which
call sees the failure is only which spawn lost the race. Nor is there
toolchain skew: every CI job installs dtolnay/rust-toolchain@stable and
the repo pins none, so curl-feature-contract compiles with the same
stable as the rest.
Two changes, both in the runner:
- EvalShellOutcome distinguishes Ran(bytes) from SpawnFailed, and
SpawnFailed answers false — php-src's own failure value (php_exec
returning FAILURE ends in RETURN_FALSE). The Ran arm is byte-for-byte
the behaviour this file always had.
- A transient spawn failure (EAGAIN/ENOMEM) is retried up to five times
with doubling backoff. Safe because it is a SPAWN failure: the child was
never created, so nothing can run twice. The classifier matches the raw
errno as well as ErrorKind, since that mapping has moved between Rust
releases and CI compiles with whatever stable is current.
Regression tests are deterministic and need no process pressure: an
over-long argument makes posix_spawn fail with E2BIG every time, on every
machine. That test asserts Bool(false) and fails with String("") against
the old runner. Its negative control pins that a command which genuinely
ran and printed nothing keeps every return value it had — without it, a
"report false whenever output is empty" fix would pass while breaking
system()/passthru(), whose successful return IS the empty-output shape.
…mped
eval_tmpfile_path() built its "unique" name from SystemTime::now().as_nanos(),
but CLOCK_REALTIME advances in whole microseconds: every caller inside one tick
got the same number. Two threads opening an ephemeral eval stream in the same
tick therefore built the identical path, and the loser's create_new failed with
AlreadyExists in the window between the winner's create and its remove_file.
open_ephemeral_stream swallows that with .ok()?, so an eval fopen("php://memory")
answered false for a stream with nothing wrong with it -- and the resource-id
tests that opened one either saw a shifted id sequence or a RuntimeFatal from the
next builtin applied to a non-resource. Looping the magician test binary reproduced
it in 13 of 800 runs, and 0 of 150 with --test-threads=1.
A process-wide AtomicU64 sequence now joins the clock, so no two calls in a
process can collide however many threads ask at once. The clock and the pid stay:
the sequence restarts at zero in each process, so they are what separate one run
and one process from the next.
The regression tests force the interleaving with a barrier instead of hoping for
it -- 8 threads realigned every round -- and both were verified to fail against
the old code (1649 distinct paths out of 2048; 274 of 2048 opens returning None).
The third test is the negative control: it pins the temp directory and the pid,
so a "fix" that used a bare global counter with a fixed name would fail it. The
barrier helper documents why its bodies must not panic: an unwinding thread never
reaches the next wait and hangs its siblings instead of failing the test.
`store_cstr` wrote each string result into a process-global `Mutex<CString>` and returned `guard.as_ptr()`. The assignment drops the `CString` the cell held, freeing the bytes any pointer handed out of that same cell still points at, and the guard is gone by the time the caller reads. The mutex serializes the write; it cannot extend the lifetime past the read. `cargo test -p elephc-pdo -- --ignored` runs the live PostgreSQL and MySQL round trips concurrently and both read SQLSTATE constantly, so one of them regularly read a freed 6-byte block that the allocator had already reused -- CI's `my_round_trip` asserted `"00000"` and got non-UTF-8 garbage. Reproduced locally at 45/300 runs against mysql:8.4 + postgres:16 containers. Every result buffer whose pointer escapes to the caller is now thread-local, the treatment `COLDATA_CELL` already had: the 26 `Mutex<CString>` cells, and `blob_cell` with its readers and writers. A thread only invalidates pointers it was itself handed, and the prelude copies each one into an owned PHP string before its next bridge call. The handle tables and the ODBC diagnostic cell stay process-global -- they hand out copies, not interior pointers. The documented contract narrows from "valid until the next call" to "valid until the next call on that thread", which is strictly weaker for callers; no symbol, signature, or return value moved, so the prelude and generated code are untouched. `sqlstate_buffers_are_isolated_between_threads` pins it without a database and fails 50/50 on the old code.
Closes the class the PDO SQLSTATE fix opened. Three more bridges handed the caller a `*const u8` / `*const c_char` into a process-global `Mutex<..>` cell and released the lock on the way out. Refilling such a cell drops or reallocates the buffer, freeing the exact bytes a pointer already in flight points at, so any second thread touching the same cell invalidates the first one's pointer -- the mechanism that reached CI as non-UTF-8 garbage in place of a SQLSTATE. - elephc-tz: `stash` and its three `CString` cells (transitions, location, abbreviations). - elephc-image: the staging and encode cells in `codec.rs` and the in/out cells in `xfer.rs` (the two `*_ptr` entry points hand out a WRITE pointer, so a concurrent `resize` there frees a buffer the prelude is still writing into), plus the per-operation state drained by later calls on the same thread: the probe result, the kv list, the float matrix, the polygon points, the IPTC groups, and the thumbnail metadata. - elephc-phar: `EXTRACT_BUFFER`, whose pointer `publish_result` returns. Handle registries stay process-global on purpose: `images()`, `draws()`, `WRITE_STREAMS`, the curl/tls/pdo tables and the magician caches are keyed by an id the program holds and hand out owned values, never an interior pointer. No ABI change: the three crates export the same 211 `extern "C"` functions with the same signatures; only the lifetime of the bytes behind the returned pointers narrows, from "until the next call" to "until the next call on that thread", which is strictly weaker for callers. One cross-thread guard per crate pins it, each verified failing on the pre-fix code: `location_buffers_are_isolated_between_threads` (3/3), `encode_buffers_are_isolated_between_threads` (3/3), and `extract_buffers_are_isolated_between_threads` (5/5, with distinct sizes AND fill bytes so either publish order is detected).
The PDO SQLSTATE use-after-free (garbage bytes for "00000" under concurrent tests) came from a static Mutex<CString> handing out as_ptr() -- the next call from any thread freed what an earlier caller was still reading. The class was swept out of pdo/tz/image/phar; this makes the sweep durable: the rule in CONTRIBUTING.md's bridge-ABI section and AGENTS.md's conventions, enforced by tests/ffi_buffer_hygiene.rs (walks crates/ and src/, flags lock-around-buffer statics through path qualifiers and Lazy/OnceLock wrappers, spares the id-keyed registry shape, shrink-only reasoned allowlist -- empty today).
`inject_if_used` / `inject_if_used_for_version` now take the `PreludeInventory` every sibling prelude takes and record the parsed declarations under the `curl` group, so declaration reachability knows which declarations the compiler contributed rather than the program. The curl prelude stays PARSE-AT-INJECT while the rest of the stdlib is now built in Rust. The deviation is stated at the injection site with the reason (PDO's conversion needed the transcriber plus the node-by-node oracle across every profile, and this surface is ~2,000 version-fenced lines) and the cost it does and does not carry: only a compile that reaches curl pays the tokenize/parse.
`--with-curl` force-injects the whole surface for a program whose only route to curl is one the compiler cannot see. Declaration reachability then deleted every one of those declarations again, because nothing in the program reaches them: measured on a curl-free fixture, 40 functions in and 1 out, with `curl_init` and `CurlHandle` both gone. Listing `curl` in `forced_groups` alongside `pdo`/`tz`/`image` is what makes the flag mean what it says. Also pins, with the measurement behind it, why NO curl declaration is registered with `record_internal_callable_method`. PDO registers four methods whose generic callable syntax only invokes closure descriptors the prelude itself created. Curl's two dynamic sites are the opposite: `curl_setopt()` asks `is_callable($value)` about the USER'S value and `__elephc_curl_sync_read_slot`'s dispatcher `call_user_func()`s the user's read callback. A callback named by bare string reaches the prelude through a `mixed` parameter that no scan connects to the declaration, so those hazards are the only thing keeping it alive — clearing them would silently delete a callback the program still calls. A third test enumerates the hazard sites, so a future genuinely internal dispatch is a failure asking the question rather than a silent answer.
The stdlib preludes are now built in Rust and their `*_PRELUDE_SRC` constants are gone, so the source-text prelude audits had nothing left to read. Both halves survive rather than one replacing the other: `injected_prelude_programs` is the single list every audit iterates, and the two that need DECLARATION TEXT render it back to PHP through `synthetic_class::print` — a rendering `printing_round_trips` re-parses and compares node for node, so an assertion made against it is as strong as one made against hand-written source. curl is the one member that is parsed rather than built, and it is in the list for that reason: a source-text prelude left out of an AST-shaped audit is silently unaudited rather than loudly unconverted. The extension-builtin call-site gate therefore covers it again, which main's conversion had dropped. `php_type_matches` grows the `TypeSpec::Ptr` and `TypeSpec::Callable` arms. Neither is a PHP scalar and neither belongs in `Mixed`'s open surface: both have exactly one spelling a prelude may use, so they compare like the scalars and are excluded from `Mixed`'s.
`__rt_report_uncaught_exception` now drains the output buffers before exiting, so it has a `call` for the first time — reached through an `and rsp, -16`. The walk tracks rsp as an exact offset from the entry and a hard realignment has no such offset, so the helper defeated it and was reported as an unnamed hole. It joins `__rt_closure_bind`, which realigns the same way. Naming it is the honest outcome rather than a weakening: the list exists to say what is NOT checked, and a `call` after `and rsp, -16` is the one construct that cannot be misaligned whatever the path in.
`src/curl_prelude.rs` is the last prelude that tokenizes and parses embedded PHP at injection time. The line records the conversion path PDO took — transcribe, keep the source as the migration oracle, gate on the node-by-node comparison across every version profile — so the deviation is a scheduled piece of work rather than an omission.
A prelude declares a PHP function in one of two shapes now: the
`function("hash_copy")` builder chain every stdlib prelude uses, or the
`function hash_copy(...)` source line `curl_prelude.rs` still ships. The
extractor knew only the second, so with the stdlib preludes built in
Rust the four `hash_*` pages resolved to `src/hash_prelude.rs:1` — an
anchor pointing at the file's module doc rather than the declaration.
The branch's loud failure is what surfaced it: matching neither form is
now an error rather than a line-1 fallback, so this could not stay
silent. Both patterns still reject a longer identifier ending in the
name, keeping `__elephc_curl_easy_body` from answering for
`curl_easy_body`.
Regenerated through the generators in the canonical `--features curl`
configuration; the four pages and their registry rows now carry real
line numbers, and everything else comes back byte-identical.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First-class PHP
ext/curlon every supported target. Programs get the full PHP 8.2–8.5 function, class, and constant surface on a checksum-pinned libcurl 8.21.0. There is no system, Homebrew, or distro-lcurl.elephc native add curl elephc app.php # or: elephc app.php --with-curlextension_loaded('curl')is true only when the bridge is linked. A curl-free program never declares the classes, never linkselephc_curl, and never needs the native package.What shipped
CURLOPT_*options work; the other 11 (blobs and three leftover callbacks) returnfalseplus PHP's warning. Never an inerttrue.curl_multi_get_handles,curl_share_init_persistent, andCurlSharePersistentHandle(gated by--php-version).CURLFile/CURLStringFile/curl_file_create, with arrayCURLOPT_POSTFIELDSas realmultipart/form-data.WRITE/HEADER/READ/PROGRESS/XFERINFO/DEBUG) through the existing runtime callable invoker. Throws abort the transfer, stay catchable, and never unwind through libcurl.CURLOPT_FILE/INFILE/WRITEHEADER/STDERRon the AOT path.eval(), except those four stream options (honestfalse+ warning).openssl_*/hash()stay on RustCrypto), zlib 1.3.2, nghttp2 1.70.0, libssh2 1.11.1.$CURL_CA_BUNDLE, then the baked path if it still exists, then seven root-owned distro locations), applied to bothCURLOPT_CAINFOandCURLOPT_PROXY_CAINFO. Explicit user options replace or compose as in a stock libcurl.User-facing contract and every documented PHP divergence:
docs/php/curl.md.Out of this PR
FROM scratch/ distroless images.CURLOPT_FILE/INFILE/WRITEHEADER/STDERRinsideeval().This is not the v0.28 Zend
curl.soconsumer PoC.extern "curl"stays user FFI.Tests / CI
curl-codegen-tests-*jobs (4 shards × 3 targets) run the ~194 codegen fixtures afterelephc native install --locked. They fail if the managed-native skip gate fires.managed-native-smokebuilds the curl stack from source and compilesexamples/curl-get.curl-feature-contract.Review notes
PHP 8 objects (
CurlHandle, …) come from an injected prelude over internal__elephc_curl_*builtins, same pattern asHashContext. The bridge owns libcurl handles by never-reusedi64ids. A few signatures throw instead of returningT|falsebecause the checker cannot narrow those unions — listed in the docs, not silent.