Zend Wrapper Layers
May 19, 2026 · View on GitHub
This document defines how vphp should isolate direct Zend C calls and expose progressively nicer V APIs above them.
Status: migration started / not fully implemented.
For the current remaining debt and "do not blindly migrate" list, see runtime_boundary_debt.md. For PHP minor-version compatibility tracking, see php_compat_inventory.md.
The current codebase already has many of these concepts. The vphp/zend/
directory now contains both C declarations and the first migrated C-boundary
wrappers, such as runtime, include, and superglobal helpers. The broader
vphp/zval/, vphp/zbox/, vphp/scope/, and vphp/object/ layout is still a
migration target and is not fully implemented yet.
Current migration checkpoint:
- Direct Zend bridge calls for runtime, include, superglobals, calls, closures,
class entries, class handlers, objects, arrays, execute data, and zval value
allocation/conversion helpers have been moved under
vphp/zend/. vphp/object/has started as a no-C low-level object wrapper area. It now contains object handle, lifecycle, property read-result, vptr root, and object return helpers. Root-levelobject_binding.vand generic lifecycle helpers now delegate their low-level allocation, roots, runtime-free, and return-object operations tovphp/object/; generated generic property helpers remain at the root because property results and field cleanup still depend on root-levelZVal/ownership and semantic wrapper types.vphp/execute/has started as a no-C low-level execute-data wrapper area. It now contains execute-data handle, argument access, and active context helpers; root-levelZExDataremains the compatibility facade because argument values still need root-levelZVal,PhpArg, and semantic wrappers.Contextnow storesZExDataandPhpReturnwrappers instead of exposing raw Zend fields. The ABI-facing constructor isContext.from_ptr(ex, ret), and generated closure glue uses that pointer-shaped entry directly.vphp/scope/has started as a no-C low-level request scope wrapper area. It now contains request mark/enter/leave helpers and autorelease zval add/forget/drain helpers; root-levelRequestScope,FrameScope, andPhpScoperemain compatibility facades because frame values still depend on root-level ZBox and semantic wrapper types.RequestBorrowedZBoxandRequestOwnedZBoxnow have pointer/handle/zval factory helpers. Legacyfrom_raw(...)/from_raw_zval(...)aliases have been removed from the request ZBox layer.vphp/zval/has started as a no-C low-level zval wrapper area. It now contains zval handle, request/persistent allocation lifecycle, copy/disown, runtime counter helpers, low-level array operation helpers, scalar/type read/write helpers, reference/resource helpers, call argument packing, scalar factory helpers, low-level foreach dispatch, include-file result handling, and static class/superglobal helpers; root-levelZValremains the compatibility facade because call result ownership and semantic wrapper methods still depend on the root-levelZVal/ZBox shape.- Root-level files still contain raw C pointer types only where they are part of
the current ABI or low-level storage shape, such as
ZVal.raw, generic object handler callbacks, foreach callback trampolines, and adapter functions that pass&C.zvalthrough tovphp/zend/.PhpReturn,ZExData,ZendObject,ZendClassEntry, and ZBox constructors now use pointer/handle shaped facades instead offrom_raw(...)/raw_zval()style public APIs. []ZVal -> []zval.Handle -> &&C.zvalargument packing is now split betweenvphp/zval/andvphp/zend/. Root-level call helpers still own result adoption becauseOwnershipKindandZValremain root-level facades.
The rule of thumb:
bridge/*.inc.c + v_bridge.c/h
-> bridge/compat.h PHP/Zend version shim
-> Zend boundary in V
-> no-C low-level V wrapper
-> abstract V semantic wrapper
The goal is not to hide Zend completely. The goal is to keep C.xxx, raw &C.zval, &&C.zval, &C.zend_object, and similar details inside a small, reviewable boundary.
vphp/bridge/ is the C implementation side. It contains C fragments included by v_bridge.c. It is not a V wrapper layer, but vphp/bridge/compat.h is the Layer 0 PHP/Zend compatibility shim that keeps PHP_VERSION_ID checks out of generated glue and V runtime wrappers.
Layer Map
| Layer | Allows | Avoids | Typical files | Typical API |
|---|---|---|---|---|
| 0. PHP/Zend compatibility shim | C code, Zend macros, PHP headers, PHP_VERSION_ID checks | V APIs, generated glue, semantic wrappers | vphp/bridge/compat.h | vphp_zend_register_constant_compat(...) |
| C implementation | C code, Zend macros, PHP headers | V APIs, scattered PHP version checks | vphp/bridge/*.inc.c, vphp/v_bridge.c, vphp/v_bridge.h | vphp_call_method(...) |
| 1. Zend C declarations | C.zval, C.zend_object, C.vphp_call_*, C.ZVAL_COPY declarations | V semantic objects | vphp/zend/types.v, vphp/zend/bridge_api.v, vphp/zend/native_api.v, vphp/zend/constants.v | pub fn C.vphp_call_method(...) |
| 2. Low-level C-boundary wrapper | Direct C.xxx, &C.zval, &&C.zval, retval allocation/release/adopt | Public semantic APIs | Target: vphp/zend/call.v, vphp/zend/value.v, vphp/zend/object.v, vphp/zend/property.v, vphp/zend/array.v | call_function_zval(...), raw_read_property(...) |
| 3. No-C low-level V wrapper | ZVal, ZExData, ZendObject, OwnershipKind, RequestScope, *ZBox | Direct C.xxx in signatures or normal call paths | Target: vphp/zval/, vphp/zbox/, vphp/scope/, vphp/object/, vphp/execute/ | ZVal.method_owned_request(...) |
| 4. Abstract V semantic wrapper | PhpValue, PhpInt, PhpString, PhpArray, PhpObject, PhpFunction, PhpArgInput, PhpArg, PhpReturn | Raw Zend types except explicit escape hatches | php_*_type.v | PhpFunction.named(...).call[T](...) |
Layer 0: PHP/Zend Compatibility Shim
Layer 0 absorbs PHP minor-version differences in the C API.
VPHP targets PHP 8.2 and newer:
#if PHP_VERSION_ID < 80200
# error "vphp requires PHP 8.2 or newer"
#endif
Allowed here:
PHP_VERSION_IDchecks- Zend API return-value differences
- Zend function signature differences
- Zend struct field or handler-signature compatibility helpers
- small inline functions or macros that normalize Zend C behavior for the rest of the bridge
Prefer PHP/Zend's public extension macros when they already provide a stable
extension-facing API. Examples include REGISTER_*_CONSTANT, PHP_FUNCTION,
PHP_MINIT_FUNCTION, and ZEND_BEGIN_ARG_*. Do not replace those macros just
to make every call look like a vphp compat helper.
Do not let PHP version branches leak into generated php_bridge.c, generated
bridge.v, vphp/zend/*.v, ZVal, ZBox, or semantic wrappers.
Add a compat helper only when vphp calls a lower-level Zend function directly and that function changes across supported PHP 8 minors. For example:
static inline bool vphp_zend_register_constant_compat(zend_constant *constant) {
#if PHP_VERSION_ID >= 80500
return zend_register_constant(constant) != NULL;
#else
return zend_register_constant(constant) == SUCCESS;
#endif
}
Call sites should only see the normalized helper:
if (!vphp_zend_register_constant_compat(&constant)) {
return FAILURE;
}
Supported PHP Versions
The intended support matrix is:
supported: PHP 8.2, 8.3, 8.4, 8.5
unsupported: PHP 8.0, 8.1
minimum: PHP_VERSION_ID >= 80200
Each supported minor should be covered by CI build/test jobs when possible.
Compatibility helpers may branch by PHP_VERSION_ID, but the V API should stay
stable across supported PHP 8 minors.
C Implementation: bridge/
vphp/bridge/ contains C implementation fragments, not V wrappers.
Current homes:
vphp/bridge/call.inc.cvphp/bridge/object.inc.cvphp/bridge/runtime.inc.cvphp/bridge/values.inc.cvphp/bridge/compat.hvphp/v_bridge.cvphp/v_bridge.h
compat.h has special ownership: it is the only intended home for PHP/Zend
minor-version shims. Other bridge fragments should call compat helpers instead
of spelling out #if PHP_VERSION_ID ... directly.
The relationship is:
bridge/*.inc.c
-> uses bridge/compat.h for PHP/Zend minor-version differences
-> compiled into v_bridge.c / v_bridge.h
-> declared to V in vphp/zend/
-> wrapped by V helpers in vphp/zend/
-> exposed as ZVal/ZBox/scope/object methods
-> surfaced as PhpValue/PhpObject/PhpFunction APIs
Do not move .inc.c files into the V wrapper directories. bridge/ should stay the C implementation home.
Layer 1: Zend C Declarations
This layer is the declaration and C implementation boundary.
It can expose raw Zend and vphp bridge symbols:
pub fn C.vphp_call_method(
obj &C.zval,
method &char,
len int,
retval &C.zval,
p_count int,
params &&C.zval
) int
This layer does not optimize for V ergonomics. It exists to keep C declarations complete, explicit, and close to the C implementation.
Current homes:
vphp/zend/types.vvphp/zend/bridge_api.vvphp/zend/native_api.vvphp/zend/constants.v
Layer 1 and Layer 2 share the vphp/zend/ directory because both are Zend-facing boundary code. The distinction is file-level:
vphp/zend/types.v -> declarations
vphp/zend/bridge_api.v -> declarations
vphp/zend/native_api.v -> declarations
vphp/zend/constants.v -> declarations
target:
vphp/zend/runtime.v -> C-boundary wrapper
vphp/zend/include.v -> C-boundary wrapper
vphp/zend/superglobals.v -> C-boundary wrapper
vphp/zend/call.v -> C-boundary wrapper
vphp/zend/closure.v -> C-boundary wrapper
vphp/zend/class_entry.v -> C-boundary wrapper
vphp/zend/class_handlers.v -> C-boundary wrapper
vphp/zend/value.v -> C-boundary wrapper
vphp/zend/object.v -> C-boundary wrapper
vphp/zend/array.v -> C-boundary wrapper
vphp/zend/execute.v -> C-boundary wrapper
vphp/zend/property.v -> C-boundary wrapper target
Layer 2: Low-Level C-Boundary Wrapper
This is the only V implementation layer where direct C.xxx usage should be common.
It owns the repetitive and error-prone patterns:
- constructing
[]&C.zval - producing
&&C.zval - allocating retval zvals
- releasing retval on failure
- adopting retval into
ZValwithOwnershipKind - handling
rv != resread-property semantics - converting V strings to
&char + len
This layer should normally stay private to vphp implementation files. It should not become a public concept for extension authors.
For example, prefer private helpers like:
fn call_function_zval(name string, args []ZVal, ownership OwnershipKind) ZVal
fn call_method_zval(receiver ZVal, method string, args []ZVal, ownership OwnershipKind) ZVal
fn call_callable_zval(callable ZVal, args []ZVal, ownership OwnershipKind) ZVal
fn new_instance_zval(class_name string, args []ZVal, ownership OwnershipKind) ZVal
These helper signatures still use only ZVal, string, and OwnershipKind. The raw &C.zval and &&C.zval details stay one level deeper:
fn call_with_args_and_ret(
args []ZVal,
ownership OwnershipKind,
run fn (&C.zval, int, &&C.zval) int
) ZVal
This helper is intentionally an implementation detail. It is not a public API.
Layer 3: No-C Low-Level V Wrapper
This layer still speaks low-level vphp, but it should not expose raw C types in normal APIs.
Allowed concepts:
ZValZExDataZendObjectOwnershipKindRequestBorrowedZBoxRequestOwnedZBoxPersistentOwnedZBoxRequestScopeFrameScope
Layer 3 should not be represented by a single zval/ directory. ZVal is only one part of this layer. The no-C low-level layer should be split by domain:
vphp/zval/
type.v
scalar.v
array.v
call.v
class.v
object_props.v
conversion.v
lifecycle.v
vphp/zbox/
types.v
factory.v
lifecycle.v
predicate.v
vphp/scope/
request.v
frame.v
vphp/execute/
data.v
args.v
vphp/object/
binding.v
generic_lifecycle.v
generic_props.v
vptr_registry.v
This document describes the target shape. The current repository still has many Layer 3 files at the root while migration is not implemented.
Typical APIs:
pub fn (v ZVal) call_owned_request(args []ZVal) ZVal {
return call_function_zval(v.to_string(), args, .owned_request)
}
pub fn (v ZVal) method_owned_request(method string, args []ZVal) ZVal {
return call_method_zval(v, method, args, .owned_request)
}
This layer is an escape hatch for vphp internals and advanced code. It is not the preferred user-facing layer, but it should already feel like V code rather than Zend C code.
Layer 4: Abstract V Semantic Wrapper
This is the layer extension and framework code should prefer.
Typical concepts:
PhpValuePhpNullPhpBoolPhpIntPhpDoublePhpStringPhpArrayPhpObjectPhpCallablePhpClosurePhpFunctionPhpClassPhpArgInputPhpArgPhpAttributePhpReturn
Typical APIs:
result := vphp.PhpFunction.named('strlen').call[i64](
vphp.PhpString.of('hello')
)!
obj.with_method_result[vphp.PhpValue]('handle', fn (value vphp.PhpValue) string {
return value.to_json()
}, vphp.PhpString.of('payload'))!
This layer can expose explicit escape hatches:
value.to_zval()
obj.to_zval()
ret.raw_ptr()
But normal code should not need those escape hatches.
PhpReturn is a special boundary wrapper: it represents Zend's callback
return_value slot. It stores a zval handle internally, and normal callers
should write through receiver methods such as ret.string_value(...),
ret.value(...), ret.zval(...), ret.owned_object(...), or
ctx.return().v(result). ret.raw_ptr() is only for bridge code that must pass
the return slot to another Zend-facing helper. Normal code should not create
&C.zval views of the return slot.
Compiler Projection
The compiler has its own internal layers, but generated code should still follow the runtime wrapper layers above.
The compiler question is slightly different:
Which runtime layer should this generated code target?
That question matters more than whether a compiler file itself lives in
parser/, builder/, or v_glue/.
Compiler Layer Map
| Compiler area | Runtime layer it may target | Should know about Zend C? | Responsibility |
|---|---|---|---|
repr/ | none directly | no | Plain data that describes PHP/V export semantics |
parser/ | none directly | no | Convert V AST and attributes into repr |
linker/ | none directly | no | Resolve relationships between repr values |
php_types/ | Layer 3 and Layer 4 facts | no C macros | Centralize type mapping, defaults, arg decoding, return facts |
arg_binding.v / params_struct_binding.v | Layer 3 or Layer 4 V glue | no direct Zend C | Generate parameter decoding through Context, PhpArg, semantic wrappers, and ZBox wrappers |
return_binding.v | Layer 3 or Layer 4 V glue | no direct Zend C | Generate return handling through PhpReturn, semantic wrappers, and explicit low-level escape hatches |
class_method_binding.v / class_property_binding.v | Layer 3 V glue, sometimes C-boundary bridge glue | limited | Generate object method/property glue; prefer wrappers where available |
v_glue*.v | Layer 3 or Layer 4 V glue | limited | Connect PHP-visible C wrappers to exported V functions/classes |
builder/ | C implementation / Zend registration code | yes, as generated text | Generate arginfo, class/function tables, module registration, attributes, properties |
c_emitter.v / c_templates.v | C implementation / Zend registration code | yes, as generated text | Generate PHP wrapper bodies and C glue that cannot be expressed as reusable builder fragments |
export.v | assembly only | no new Zend logic | Assemble fragments into php_bridge.c, php_bridge.h, and bridge.v |
Repr And Parser
repr/ and parser/ should describe intent, not Zend implementation.
Good repr fields:
- PHP name
- V type
- PHP type hint
- optional/default metadata
- attributes
- visibility
- return shape
- ownership intent
Poor repr fields:
zend_longZEND_ACC_PUBLICZEND_ARG_TYPE_INFO- raw
&C.zval - pre-rendered C lines
Those details belong in builder/, c_emitter.v, or lower runtime wrappers.
php_types As Type Semantics
php_types/ should be the compiler's type facts center.
It should answer questions like:
- Which PHP type hint does this V type imply?
- Is this argument optional?
- Which
PhpArgmethod decodes this semantic wrapper? - Which default literal is valid for this type?
- Which return path should be used for this type?
It should not render Zend C macros directly. For example, php_types/ may say
that PhpArray maps to PHP array, but builder/arginfo.v should decide how
that becomes ZEND_ARG_TYPE_INFO(...).
Builder And C Emitter
builder/ and c_emitter.v are allowed to generate Zend C API usage.
They are the compiler-side equivalent of the C implementation and Zend registration boundary:
builder/arginfo.v
builder/class.v
builder/function.v
builder/module.v
c_emitter.v
c_templates.v
These files may generate:
ZEND_BEGIN_ARG_*ZEND_ARG_*zend_register_internal_classzend_declare_property_*zend_add_class_attributezend_add_parameter_attributePHP_FUNCTIONPHP_METHOD
The important rule is that Zend C details should stay concentrated here. If multiple builder files need the same Zend pattern, prefer a small builder helper over duplicating generated C strings.
V Glue
Generated bridge.v should prefer no-C or semantic wrappers.
Preferred generated forms:
ctx := vphp.Context.from_ptr(ex, ret)
php_args := ctx.args_with_meta([...])
name := php_args.at_named_or_index(0, 'name').string_value() or { ... }
ctx.return().string_value(result)
For Zend ABI callbacks, raw C pointer types are acceptable only at the exported signature boundary. The generated body should immediately create wrapper values:
fn generated_bridge(ex &C.zend_execute_data, ret &C.zval) {
ctx := vphp.Context.from_ptr(ex, ret)
...
}
fn generated_get_prop(ptr voidptr, name_ptr &char, name_len int, rv &C.zval) {
ret := vphp.PhpObjectPropertyHandler.return_from_ptr(rv)
name := vphp.PhpObjectPropertyHandler.name_from_ptr(name_ptr, name_len)
...
}
Acceptable low-level forms:
raw := ctx.arg[vphp.ZVal](0)
box := ctx.arg[vphp.RequestBorrowedZBox](0)
Less desirable forms:
mut rv := C.zval{}
C.vphp_read_property_compat(...)
C.vphp_write_property_compat(...)
These are still sometimes necessary today, especially in class/object glue, but
they should be treated as migration targets. When the generated V glue needs
direct C.xxx, that usually means one of these wrappers is missing:
ZExDataPhpReturnZendObject/PhpObjectPropertyHandler- class/object lifecycle helper
ZVal/ZBoxconversion helper
Generated Code Review Rules
When reviewing compiler changes, ask:
- Does parser/repr contain generated C details?
- Is a type mapping duplicated outside
php_types/? - Does
builder/duplicate a Zend registration pattern that should be a helper? - Does generated
bridge.vuseC.xxxwhereContext,PhpArg,PhpReturn,ZVal, orZBoxwould be enough? - Does generated user-facing glue force extension authors down to
ZValwhen a semantic wrapper exists? - Is a new compiler concept just mirroring a runtime semantic type with a worse name?
Migration Priority
This compiler projection is also a migration plan.
Near-term, low-risk cleanup:
- keep adding argument and return behavior to
arg_binding.vandreturn_binding.v - keep PHP-facing type facts in
php_types/ - move repeated attribute, arginfo, property, and class registration snippets into builder helpers
- make generated glue prefer
PhpArg,PhpReturn, and semantic wrappers
Medium-term cleanup:
- reduce direct
C.xxxinv_glue_class.vandclass_property_binding.v - introduce missing
ZExDataand object property wrappers before changing generated glue shape - split large C templates only when a repeated pattern has a clear owner
Non-goals for now:
- a large compiler directory reshuffle
- forcing every generated glue path to avoid
ZVal - hiding all Zend concepts from
builder/andc_emitter.v - inventing new public runtime types only to make generated code look nicer
Deep-Water Checklist
The next migration stage is risky because it touches both runtime ownership and compiler-generated glue. Before moving files or changing public names, keep these boundaries explicit.
What Can Move First
These are good next targets because they can be improved without changing the extension author API:
ZValobject property helpers can delegate throughZendObject, so property read/write semantics have one owner.- static property and class constant reads can keep sharing ownership-parametrized private helpers.
- call helpers can keep the current
call_zval_target(...)shape while progressively gaining clearer private target wrappers likecall_callable_zval(...),call_method_zval(...), andconstruct_zval(...). - compiler-generated code should continue moving from raw return helpers toward
Context.from_ptr(...),ctx.return(),ZExData,ZendObject,ZendClassEntry, and semantic wrappers.
What Should Not Move Yet
These areas should not be moved into submodules until the dependency direction is solved:
- Layer 2 helpers that need
ZVal,OwnershipKind, orZBoxcannot simply move intovphp/zend/today, becausevphp.zendis already imported by the parentvphpmodule for C declarations. Importing parent runtime types back fromvphp.zendwould create the wrong dependency direction. - generated Zend callback signatures still need raw
&C.zend_execute_data,&C.zval,&C.zend_object, and similar types at the ABI edge. The important cleanup is to wrap them immediately inside the generated function body. - The former
raw_zval(),raw_ex(),from_raw(...),return_*_raw(...), and object binding raw helper surface has been narrowed or removed. Do not reintroduce public raw aliases unless the caller is an unavoidable ABI edge; preferfrom_ptr(...),from_handle(...),raw_ptr(), or semantic wrappers.
Allowed Raw Boundaries
The remaining raw C types are intentional and should stay small and reviewable:
ZVal.rawand lifecycle helpers inzval.v/zval_lifecycle_interop.v, because they own allocation, copy, release, disown, and adoption semantics.zval.Handle.raw_ptr(),object.Handle.raw_ptr(), andexecute.Handle.raw_ptr(), because Layer 3 needs one narrow pointer exit to call Layer 2.- Generated or generic Zend callback signatures, such as
object_generic_props.vandzval_factory_iter.v, because Zend calls into V with&C.zval/callback context pointers. These functions should wrap raw inputs immediately withPhpObjectPropertyHandler,ZVal.from_ptr(...), or handle wrappers. vphp/zend/**, because it is the explicit C-boundary wrapper layer.
Outside those areas, new raw &C.zval, &C.zend_object, C.xxx, or public
from_raw(...) style APIs should be treated as a design smell.
Ownership-Sensitive Areas
Treat these as high-risk until each change has a focused lifecycle test:
adopt_read_result(rv, res, ownership), because Zend property reads may return either the scratchrvor another zval pointer.prop_borrowed(...), because the name is easy to misuse if the returned value escapes beyond the immediate read/copy path.- persistent/request conversion on
PhpValue,PhpArray,PhpObject, andDynValue. - closure binding and callable invocation, because a captured V function may outlive the request frame that created the PHP callable wrapper.
- object binding and retained object/callable roots, because those paths decide who releases V-side payloads.
Test Gate For Deep Edits
For code changes in the areas above, run at least:
vphptest make build
vphptest ./run_tests.sh
LIBRARY_PATH=/opt/homebrew/opt/libsodium/lib make -C vslim build
If ownership changes, also run the lifecycle/probe scripts under
vphptest/tests/ and any relevant VSlim request-loop probes before committing.
Review Rules
When reviewing new vphp code, ask:
- Which layer does this file belong to?
- Does this file expose a lower-layer concept in its public API?
- Does a semantic wrapper directly call
C.xxxwhen a lower layer should own that call? - Does compiler-generated code emit
C.xxxwhere a no-C wrapper would be clearer? - Is a new public type being introduced for an implementation detail that could stay private?
The most important smell:
Layer 4 code directly using Layer 1/2 C boundary calls.
That is usually a sign that a Layer 2 or Layer 3 helper is missing.
Migration Strategy
Status: partially started. The first low-risk compiler/runtime cleanup makes
generated return glue prefer ctx.return().* / PhpReturn over older
Context.return_* convenience calls. ZExData now owns the low-level argument
read helpers that Context delegates to. Generated property glue now uses
PhpReturn and ZVal.from_ptr(...) instead of direct return/raw-zval helpers
inside the generated body, and inherited object property sync goes through
ZendObject instead of direct vphp_read/write_property_compat calls in the
generated body. Inherited property reads now use
ZendObject.prop_owned_request(...), so generated V glue no longer emits
C.zval{} scratch values for that path. Generated class handler exports now
use ZendClassHandlers.new(...) instead of constructing C.vphp_class_handlers
directly in generated bridge.v. ZVal call/construct/static-method paths now
share a private call_zval_target(...) helper, so retval allocation,
argv conversion, Zend result handling, and ownership adoption live in one
place. Call intent is represented by a private ZendCallTarget, keeping raw
&C.zval / &&C.zval argv details inside the innermost call boundary.
Inherited scalar property loading uses ZendObject.prop_*_or(...)
helpers that borrow only inside the wrapper and immediately copy into V scalar
values; generated code no longer needs prop_owned_request(...) for that path.
PhpIncludeFile now shares the same request-owned adoption path as other
retval-producing helpers, and static property/class constant reads share
ownership-parametrized helpers. Generic property handlers use PhpReturn and
ZVal.from_ptr(...) instead of older raw helper forms. PhpReturn and object
property convenience paths also prefer ZVal.from_ptr(...) and scalar receiver
methods over direct raw field construction where no ownership semantics change.
Array/object write helpers such as associative zval insertion, next-value
insertion, object initialization, and string property updates are also exposed as
ZVal receiver methods, so semantic wrappers can avoid direct C.vphp_array_*
and C.vphp_object_* calls for those paths. Request-scope autorelease calls,
active extension globals access, object metadata reads, and ZVal callable/class
invocation bridge calls are now routed through private Zend wrapper helpers.
The previously declared-but-unimplemented
vphp_allocate_contiguous_object(...) C bridge now has a real declaration and
implementation, with V access routed through zend_allocate_contiguous_object.
Direct PHP allocator usage for saved closures and task handles is now routed
through private zend_emalloc / zend_efree helpers. ZVal release and disown
paths now use private zend_release_zval* / zend_disown_zval helpers, so
failure cleanup in include/call/read helpers no longer calls the release C API
directly. ZVal allocation paths now use private zend_new_zval* helpers.
RetainedObject now stores a ZendObject wrapper instead of a raw
&C.zend_object, keeping refcount operations behind ZendObject receiver
methods. DynValue array/list encoding and PhpReturn.list(...) struct encoding
now keep their temporary stack C.zval{} values inside ZVal helpers, instead
of leaking scratch-zval construction into semantic wrapper code. Generic object
payload release now calls a private V-runtime free helper instead of directly
calling C.builtin___v_free(...). ZVal object initialization and scalar
property insertion now delegate through private Zend object helpers instead of
calling Zend property APIs directly from zval_object_props.v. Closure creation
helpers now accept PhpReturn, so closure binding code no longer has to pass
raw return slots down manually. ZExData receiver methods now delegate
through private Zend execute-data helpers for argument count, argument lookup,
active class lookup, and $this object lookup. Compiler-generated static
property sync now uses ctx.active_class_entry() and ZendClassEntry receiver
methods directly; the old global set_static_prop(...) / get_static_prop(...)
voidptr helpers have been removed. ZVal array and scalar receiver methods now
delegate through private zend_array_* and zend_zval_* helpers, keeping
bridge calls grouped inside the low-level wrapper files instead of spread across
each public receiver method. ZVal reference, resource, and type/callable
receiver methods follow the same pattern through private Zend helpers. ZVal
factory and foreach helpers also route creation and iteration bridge calls
through private helpers, leaving the callback wrappers as the explicit ABI edge.
ZVal copy/adoption paths now use a private zend_copy_zval(...) helper instead
of spelling ZVAL_COPY at each copy site. The old V-side
return_*_object_raw(...) helpers have been removed; object return paths now
flow through PhpReturn receiver methods and private object-return helpers that
accept PhpReturn directly. ZendClassEntry static-property receiver methods,
include-file helpers, and superglobal helpers now also route their bridge calls
through narrower private helpers, keeping each public wrapper method focused on
V-facing behavior. Unused global object-binding aliases were removed in favor
of ZVal receiver methods, and ZendObject receiver methods now delegate
refcount, handler binding, wrapper lookup, and existing-object wrapping through
private Zend object helpers. Runtime task entrypoints now write returns through
ctx.return() / PhpReturn receiver methods instead of the older
ctx.return_* convenience API. Unused receiver helpers that returned
&C.vphp_object_wrapper were removed from object_binding.v; binding remains
available through the higher-level ZVal.bind_object(...) receiver API.
Runtime, include-file, and superglobal C-boundary implementations have started
moving into vphp/zend/, with parent-module files kept as thin adapters where
they still need ZVal, ZBox, or semantic wrapper types. The broader layer
migration should continue incrementally.
Context.from_entry(...), Context.from_raw(...), ZExData.new(...),
ZExData.raw_ex(), PhpReturn.raw_zval(), ZVal.from_raw(...),
ZendObject.from_raw(...), ZendClassEntry.from_raw(...), and request-ZBox
from_raw(...) compatibility aliases have been removed; pointer and handle
constructors are now the supported low-level shape.
First: Call Paths
Status: partially started.
ZVal call, method, construct, and static-method paths now share
call_zval_target(...), so argv conversion, retval allocation, Zend call
result handling, and ownership adoption are no longer repeated in every method.
The previous raw callback shape has been replaced by a private
ZendCallTarget, and &C.zval / &&C.zval argv details are confined to
call_zval_target(...) and invoke_zval_call_target(...).
The direct C.vphp_call_*, C.vphp_new_instance(...), and static
property/class-constant bridge calls are routed through private Zend wrapper
helpers. The remaining work is to move the helper file into the eventual
vphp/zend/ layout when dependency direction allows it.
Start with:
vphp/zval_call_interop.vvphp/zval_class_interop.v- callers in
vphp/php_function_type.v - callers in
vphp/php_object_type.v
Target:
- keep repeated
argv/p_argsconstruction centralized - keep repeated
retval/release/adoptlogic centralized - keep
ZendCallTargetprivate - keep public APIs unchanged
- do not introduce public
ZendCallArgsorZendCallResult
Preferred shape:
PhpFunction.call(...)
-> php_arg_inputs_to_zvals(...)
-> ZVal.call_owned_request(...)
-> call_function_zval(...)
-> call_zval_target(...)
-> invoke_zval_call_target(...)
-> C.vphp_call_php_func(...)
Second: Property And Class Reads
Status: partially started.
ZendObject now owns inherited object property reads for generated glue, and
static property/class constant reads share ownership-parametrized private
helpers. ZVal object class-name, parent-class-name, and internal/user-class
metadata reads now delegate to private Zend object helpers. The remaining work
is to make older ZVal object-property helpers delegate through ZendObject
where that does not change ownership semantics.
Then handle:
vphp/zval_object_props.v- static property helpers
- class constant helpers
- compiler-generated property glue
Be careful with rv and res ownership. Existing helper adopt_read_result() handles the important boundary where the returned pointer may or may not be the temporary rv.
Third: Compiler Output
Status: partially started.
Generated return glue now prefers ctx.return() / PhpReturn, and generated
argument reads increasingly flow through Context, ZExData, PhpArg, and
semantic wrappers. Generated property glue now wraps property-handler raw
inputs through PhpObjectPropertyHandler, and struct-param closure bridges
delegate callback invocation and return writing through
Context.invoke_struct_closure*. Direct C signatures still exist at Zend
callback edges, especially exported function/method wrappers, closure bridges,
and generated class property handlers. Those signatures are still necessary
boundary points until a dedicated compiler-boundary abstraction exists.
Only after runtime helpers are stable should compiler output change.
The compiler should eventually prefer V wrappers over direct C.vphp_* calls where possible, but generated glue may still need explicit C types for Zend callback signatures.
Naming Guidance
Avoid naming private implementation helpers as new public concepts.
For example, avoid public types like:
ZendCallArgs
ZendCallResult
unless extension authors genuinely need to use them.
Prefer private helpers named after what they do:
fn call_function_zval(...)
fn call_method_zval(...)
fn call_with_args_and_ret(...)
fn read_property_zval(...)
If a file intentionally owns the C boundary, name it plainly:
vphp/zend/call.v
vphp/zend/closure.v
vphp/zend/class_entry.v
vphp/zend/object.v
vphp/zend/property.v
vphp/zend/array.v
The name should make it obvious that direct C.xxx is expected inside that file and suspicious elsewhere.
中文说明
这份文档定义 vphp 如何隔离 Zend C 调用,并在其上逐层提供更适合 V 的 API。
状态:迁移已开始 / 尚未完整实现。
当前代码已经有不少相关概念,vphp/zend/ 也已经开始承载 C-boundary wrapper,例如 runtime.v、include.v、superglobals.v、call.v、closure.v、class_entry.v、class_handlers.v、object.v、array.v、value.v、execute.v。但 vphp/zval/、vphp/zbox/、vphp/scope/、vphp/object/ 这些目标目录仍未完整落地。
当前迁移检查点:
- runtime、include、superglobals、call、closure、class entry、class handlers、object、array、execute data、zval value 分配/转换相关的直接 Zend bridge 调用已经迁入
vphp/zend/。 vphp/object/已经作为 no-C low-level object wrapper 区域开始落地。目前包含 object handle、lifecycle、property read-result、vptr root 与 object return helper;根层object_binding.v和 generic lifecycle helper 已把低层 allocation、roots、runtime-free、return-object 操作委托给vphp/object/;generated generic property helper 仍保留在根层,因为 property result 与 field cleanup 仍依赖根层ZVal/ownership 和语义 wrapper 类型。vphp/execute/已经作为 no-C low-level execute-data wrapper 区域开始落地。目前包含 execute-data handle、argument access 与 active context helper;根层ZExData仍作为兼容 facade 保留,因为 argument value 仍需要根层ZVal、PhpArg与语义 wrapper。Context现在持有ZExData与PhpReturnwrapper,不再暴露 raw Zend 字段。ABI-facing constructor 是Context.from_ptr(ex, ret),generated closure glue 也直接使用这个 pointer 形态入口。vphp/scope/已经作为 no-C low-level request scope wrapper 区域开始落地。目前包含 request mark/enter/leave helper 与 autorelease zval add/forget/drain helper;根层RequestScope、FrameScope、PhpScope仍作为兼容 facade 保留,因为 frame value 仍依赖根层 ZBox 与语义 wrapper 类型。RequestBorrowedZBox与RequestOwnedZBox现在提供 pointer/handle/zval factory helper。旧的from_raw(...)/from_raw_zval(...)alias 已从 request ZBox 层移除。vphp/zval/已经作为 no-C low-level zval wrapper 区域开始落地。目前包含 zval handle、request/persistent allocation lifecycle、copy/disown、runtime counter helper、低层 array operation helper、scalar/type read/write helper、reference/resource helper、call argument packing、scalar factory helper、低层 foreach dispatch、include-file result handling、static class helper 与 superglobal helper;根层ZVal仍作为兼容 facade 保留,因为 call result ownership 与语义 wrapper 方法仍依赖根层ZVal/ZBox 形态。- 根层文件仍会保留一部分 raw C pointer 类型,但只应出现在 ABI 或低层存储形态中,例如
ZVal.raw、generic object handler callback、foreach callback trampoline,以及透传&C.zval到vphp/zend/的 adapter。PhpReturn、ZExData、ZendObject、ZendClassEntry与 ZBox constructor 已统一走 pointer/handle 形态,不再提供from_raw(...)/raw_zval()风格的公开 API。 []ZVal -> []zval.Handle -> &&C.zval的参数打包现在已经拆到vphp/zval/与vphp/zend/之间。根层 call helper 仍负责 result adoption,因为OwnershipKind与ZVal仍是根层 facade。
核心规则:
bridge/*.inc.c + v_bridge.c/h
-> bridge/compat.h PHP/Zend version shim
-> V 侧 Zend boundary
-> no-C low-level V wrapper
-> abstract V semantic wrapper
目标不是完全隐藏 Zend,而是让 C.xxx、&C.zval、&&C.zval、&C.zend_object 这些细节只出现在少数可审计的边界文件里。
层次含义:
- Layer 0:
vphp/bridge/compat.h,PHP/Zend C API 版本兼容层。这里允许PHP_VERSION_ID判断,用来吸收 PHP 8 小版本之间的 Zend API 差异。 - C implementation:
vphp/bridge/和v_bridge.c/h,负责 C bridge 实现碎片;普通 bridge fragment 不属于 V wrapper 层,也不应该散写 PHP 版本判断。 - Layer 1:
vphp/zend/里的 C 声明文件,如types.v、bridge_api.v、native_api.v、constants.v。 - Layer 2:同样位于
vphp/zend/,但文件是 wrapper,如call.v、object.v、property.v、array.v。这是唯一集中接触C.xxx的 V 实现层。 - Layer 3:按领域拆成
vphp/zval/、vphp/zbox/、vphp/scope/、vphp/object/、vphp/execute/,不在签名和常规调用路径里暴露C.xxx。 - Layer 4:扩展作者优先使用的语义层,即
PhpValue、PhpString、PhpArray、PhpObject、PhpFunction、PhpArgInput、PhpArg、PhpReturn等。
PHP 版本策略:
支持:PHP 8.2、8.3、8.4、8.5
不支持:PHP 8.0、8.1
最低版本:PHP_VERSION_ID >= 80200
原则是:#if PHP_VERSION_ID ... 只能集中在 Layer 0。generated php_bridge.c、generated bridge.v、vphp/zend/*.v、ZVal/ZBox 与语义 wrapper 都不应该感知 PHP 小版本差异。
如果 PHP/Zend 已经提供了稳定的 extension-facing 宏,优先使用官方宏,例如 REGISTER_*_CONSTANT、PHP_FUNCTION、PHP_MINIT_FUNCTION、ZEND_BEGIN_ARG_*。不要为了“统一”把这些官方宏替换成 vphp 自己的 compat helper。
只有当 vphp 直接调用底层 Zend 函数,且这个函数在支持的 PHP 8 小版本之间发生签名或返回值变化时,才在 compat.h 里收口成稳定 helper。例如直接调用 zend_register_constant 时:
static inline bool vphp_zend_register_constant_compat(zend_constant *constant) {
#if PHP_VERSION_ID >= 80500
return zend_register_constant(constant) != NULL;
#else
return zend_register_constant(constant) == SUCCESS;
#endif
}
调用点只使用稳定语义:
if (!vphp_zend_register_constant_compat(&constant)) {
return FAILURE;
}
review 时最重要的问题是:
这个文件属于哪一层?有没有越层暴露低层概念?
最明显的坏味道是:
Layer 4 semantic wrapper 直接调用 Layer 1/2 C boundary API。
这通常说明缺一个 Layer 2 或 Layer 3 helper。
允许保留的 raw 边界要保持很小:
zval.v/zval_lifecycle_interop.v里的ZVal.raw与生命周期 helper,因为它们负责 allocation、copy、release、disown、adoption 语义。zval.Handle.raw_ptr()、object.Handle.raw_ptr()、execute.Handle.raw_ptr(),因为 Layer 3 需要一个窄出口调用 Layer 2。- Zend callback signature,例如
object_generic_props.v与zval_factory_iter.v,因为 Zend 调进 V 时天然传入&C.zval/ callback context pointer。这些函数体内必须立即包成ZVal.from_ptr(...)、PhpReturn.from_ptr(...)或 handle wrapper。 vphp/zend/**,因为它就是显式 C-boundary wrapper 层。
这些区域之外,如果新增 &C.zval、&C.zend_object、C.xxx 或公开 from_raw(...) 风格 API,默认应视为设计坏味道。
深水区迁移时尤其要注意:
ZVal对象属性读写可以继续收口到ZendObject,这是低风险方向。call_zval_target(...)已经把调用路径里的 argv、retval、ownership adoption 集中了,调用意图由私有ZendCallTarget表达;下一步重点是边界归属,而不是发明公开的ZendCallArgs。- 需要
ZVal、OwnershipKind、ZBox的 Layer 2 helper 现在不能直接搬进vphp/zend/子模块,因为vphp.zend已经是 parent module 的 C 声明依赖,反向引用 runtime 类型会制造依赖方向问题。 prop_borrowed(...)、adopt_read_result(...)、persistent/request 转换、closure binding、retained object/callable roots 都属于 ownership 敏感区,每次修改都要跑完整 vphptest 和相关生命周期探针。