Export, C Emitter, and V Glue
May 19, 2026 ยท View on GitHub
Goal
This document explains how these three files collaborate:
They all participate in code generation, but they do not have the same job.
That distinction is important.
High-Level Roles
export.v
Role:
- assembly
- file emission
- fragment collection
It decides:
- what gets collected
- in which order
- into which output file
It should not be responsible for:
- deep wrapper templates
- AST interpretation
- low-level PHP runtime bridge logic
c_emitter.v
Role:
- concrete C wrapper emission
It decides:
- how PHP methods/functions are wrapped in C
- which wrapper template a symbol needs
- how builder fragments are combined with implementation bodies
It is the most C-specific generation file.
v_glue.v
Role:
- V-side bridge emission
It decides:
- how exported wrappers call back into V code
- how PHP data is converted and forwarded through
vphp.Context - how class shadow sync helpers are emitted
- how tasks are registered
It is the most runtime-bridge-specific generation file.
Mental Model
Use this split:
export.v= coordinatorc_emitter.v= C implementation authorv_glue.v= V implementation author
Output Ownership
export.v
Owns writing:
php_bridge.hphp_bridge.cbridge.vtrigger path
It does not invent all content itself. It collects and assembles content from other layers.
c_emitter.v
Contributes content for:
- declarations/fragments via builders
- C implementations via wrapper generation
v_glue.v
Produces:
- the entire
bridge.vcontent
This is why export.v still calls into VGenerator.
Data Flow
flowchart TD
A["repr + linker results"] --> B["c_emitter maps repr to builders"]
B --> C["builders emit ExportFragments"]
C --> D["export.v assembles php_bridge.h/php_bridge.c"]
A --> E["v_glue emits bridge.v"]
More concretely:
compile()finalizeselementsexport.vasks for non-type and type fragmentsc_emitter.vbuilds builders and fills implementation fragmentsModuleBuilderassembles final C module sectionsv_glue.vemits the V-side bridge file
export.v in Detail
Main responsibilities
- collect non-type fragments
- collect type fragments
- write header declarations
- write implementation sections
- pass function/minit fragments into
ModuleBuilder - render final extension-level C blocks
- generate
bridge.v
Why fragment collection is split
export.v currently separates:
- non-type fragments
- type fragments
This keeps ordering explicit.
That matters because interface/type ordering is semantically important during registration.
Why export.v should stay boring
This file is healthiest when it mostly:
- collects
- merges
- writes
If logic here starts deciding method wrapper templates or parsing meaning from reprs, the boundaries are drifting.
V Glue Boundary Rule
bridge.v is allowed to expose Zend ABI shapes at exported callback entry
points, because Zend calls those functions with raw pointers:
fn bridge_name(ex &C.zend_execute_data, ret &C.zval) {
ctx := vphp.Context.from_ptr(ex, ret)
...
}
Object handler glue follows the same rule:
fn class_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)
...
}
The raw pointer should not spread past that boundary line. Generated V glue
should prefer Context, PhpReturn, PhpObjectPropertyHandler, ZVal,
ZendObject, ZendClassEntry, and semantic Php* wrappers. New generated
uses of Context.from_raw(...), raw_zval(), raw_ex(), ZVal.from_raw(...),
direct C.vphp_*, or manual C.zval{} construction should be treated as
migration regressions.
c_emitter.v in Detail
Main responsibilities
- map
reprinto builders - produce builder-backed fragments
- generate full C wrapper bodies for:
- functions
- classes
- interfaces
- enums
Why builder + emitter both exist
Because not all generated C is equally reusable.
Examples:
zend_class_entrydeclarations are reusable builder outputPHP_METHOD(...)wrapper bodies are still highly template-specific emitter output
So the current design is:
- builder handles normalized boilerplate
- emitter handles concrete wrapper bodies
Typical pattern
For a class export:
- convert
PhpClassReprintoClassBuilder - ask builder for fragments
- fill
implementationswith wrapper bodies fromgen_class_c(...) - hand the whole thing back to
export.v
That is why build_class_export(...) exists alongside build_class_type(...).
Return-shape classification
One subtle but important responsibility in c_emitter.v is deciding whether a
@[php_method] return value should be emitted as:
- an object-return wrapper, or
- a plain value/container bridge using
ctx.return().v[...]
This classification must stay aligned with v_glue.v.
Object-return wrappers are only correct for:
- constructors (
construct/init) - static factory methods returning the receiver type
- methods returning
&SomePhpClass
Container returns such as:
map[string]stringmap[string]int[]string
are still value returns, even though TypeMap may fall back to a generic C
representation for them.
If c_emitter.v misclassifies these as object returns, generated C will try to
emit synthetic class-entry symbols such as:
map[string]string_ce[]string_ce
which are both invalid C identifiers and the wrong runtime model.
The practical rule is:
- object wrappers are chosen from method semantics
- generic fallback C types alone are not enough to imply "PHP object return"
What should eventually move out of c_emitter.v
Potential future refactors:
- richer arginfo modeling
- more method-table scaffolding
- more common class implementation prelude/postlude
What should probably stay:
- wrapper template selection
- object-return vs scalar-return wrapper branching
- PHP runtime bridge-specific C details
v_glue.v in Detail
Main responsibilities
- emit global function glue
- emit class glue
- emit task registration glue
- emit class shadow sync helpers
Why it is separate from c_emitter.v
Even though both are "generators", they operate in different worlds:
c_emitter.vtargets Zend/C ABIv_glue.vtargets V runtime semantics
Keeping them separate preserves a clean mental boundary between:
- PHP-facing wrappers
- V-facing wrappers
Important responsibilities unique to v_glue.v
Context-based argument extraction- calling the correct V symbol, including original name remapping
- class handler export generation
- shadow static sync code generation
- task registration emission
Why v_glue.v still feels large
Because it currently hosts three domains together:
- function glue
- class glue
- task glue
This is a reasonable next candidate for future splitting.
Collaboration by Symbol Kind
Global function
- parser creates
PhpFuncRepr c_emitter.vcreatesFuncBuilderFuncBuildercontributes:- declaration
- function table entry
c_emitter.vcontributes:- C wrapper implementation
v_glue.vcontributes:- V-side
vphp_wrap_xxx(...)
- V-side
Class
- parser creates
PhpClassRepr - linker may append shadow-derived constants/properties
c_emitter.vcreatesClassBuilderClassBuildercontributes:- class entry declaration
MINITregistration
c_emitter.vcontributes:PHP_METHOD(...)wrappers
v_glue.vcontributes:- V-side wrapper functions
- handlers
- property sync
- shadow sync helpers
Interface
- parser creates
PhpInterfaceRepr c_emitter.vcreatesClassBuilderwithClassType.interface_- builder contributes:
- declaration
- interface registration
- emitter contributes:
- method metadata scaffolding
Enum
- parser creates
PhpEnumRepr c_emitter.vcreatesClassBuilderwithClassType.enum_- builder contributes:
- declaration
- registration
- constants
- emitter contributes:
- enum constructor blocking wrapper
Boundary Rules
These rules help keep the generation layer understandable.
export.v should
- orchestrate
- merge
- write files
export.v should not
- invent wrapper template policy
- inspect AST
- own class/function semantics
c_emitter.v should
- own C wrapper body generation
- translate finalized reprs into builder usage
c_emitter.v should not
- parse AST
- write final files directly
- own module-level orchestration
v_glue.v should
- own V bridge generation
- translate finalized reprs into V wrapper glue
v_glue.v should not
- register PHP classes directly
- build final C module entry output
Current Pain Points
These are the current tradeoffs, not necessarily mistakes.
export.vstill manually performs two fragment collection passesc_emitter.vremains template-heavyv_glue.vstill mixes multiple glue domains in one file- some builder/emitter boundaries are still evolving
Good Next Refactors
These would fit the current architecture well.
For export.v
- introduce slightly richer export collection helpers
- reduce repeated collection patterns
For c_emitter.v
- split by symbol family later:
- function emitter
- class emitter
- enum/interface emitter
For v_glue.v
- split by glue family later:
- function glue
- class glue
- task glue
These should happen only when the boundaries are stable enough to deserve separate modules or files.
Summary
The three files work best when each keeps a narrow role:
export.vassemblesc_emitter.vemits PHP-facing C wrappersv_glue.vemits V-facing bridge code
That separation is what keeps the compiler from collapsing back into one giant generation file.