Menu
Guides
- Install
- CLI reference
- The superset
- Differences from PHP
- Async
- Modules
- FFI
- Attributes
- Generics
- Memory model
- Typed arrays (Ds)
- HTTP server
- WebSocket
- PDO
- curl
Reference
Internals
Building projects with Manticore — the module system
Manticore builds multi-file PHP projects from a single cargo-style manifest,
manticore.json. One manifest defines every artifact — your application
binaries and any libraries they link — and the same definition drives both a
normal build and the compiler's own self-rebuild.
This is an end-user guide. For the internal design rationale see
docs/design/build-and-packaging.md.
The manifest#
manticore.json at your project root declares two kinds of target:
{
"libraries": [{
"name": "util",
"src": "src/Util",
"output": "lib/util.o",
"exclude": []
}],
"applications": [{
"name": "app",
"src": "src",
"output": "bin/app",
"entry": "src/main.php",
"exclude": ["src/Util"],
"libraries": ["util"]
}]
}Build everything:
manticore build # uses ./manticore.json
manticore build path/to.json # explicit manifest
manticore build --libs-only # build only the library targets, skip appsbuild compiles every libraries[] entry first (each → a standalone .o
plus an .o.sig interface), then every applications[] entry (→ an
executable that imports its dependencies' .sig and links their .o).
Application fields#
| Field | Meaning | |
|---|---|---|
name | label, shown in build output | |
src | directory scanned recursively for *.php | |
output | path of the linked executable | |
entry | optional — the file whose top-level code becomes main(). Every other file contributes only its declarations. Omit it and files load in `find … | sortorder (azzz_*` driver sorts last by convention). |
exclude | optional — path prefixes to skip in the src scan (e.g. a subtree compiled separately as a library) | |
libraries | optional — which user library targets to import + link. Omit ⇒ all, [] ⇒ none, a named subset ⇒ just those. Independent of the stdlib (see below). | |
stdlib | optional — set false to opt OUT of the always-on stdlib runtime. Only the self-contained compiler (which embeds src/Runtime) needs this. | |
composer | optional — true also compiles the Composer view of the project (see below); the object form { "vendor": false } narrows it | |
extensions | optional — names of native extensions to bind and link. Every name must exist in the manifest's top-level extensions map, or the build fails |
Library fields#
name, src, output, exclude — same as above. A library is compiled with
--emit-library: a standalone .o with no main(), plus <output>.sig.
It declares the stdlib's functions (so a call into the stdlib — its own or a
prelude body's — is an extern reference resolved when the application links the
stdlib), and an application that links any library links the stdlib too.
A library marked "runtime": true is the bundled stdlib — built, but treated as
the always-on runtime rather than a normal dependency (see below); it declares
nothing from itself. The CLI spelling is compile --emit-library --runtime.
Two independent opt-outs: "stdlib" and "libraries"#
They are not the same switch, and confusing them is the usual manifest bug.
"stdlib": falseon an application opts out of the always-on runtime. A"runtime": truelibrary is injected and linked into every app regardless of thelibrariesselection, so"libraries": ["mylib"]can never accidentally drop the stdlib. You opt out only when your app already embeds that source."libraries"selects among the other (non-runtime) libraries: omit the key ⇒ all of them,[]⇒ none, a list of names ⇒ just those. An application that embeds a copy of a library's source must exclude it here, or the same symbols are defined twice.
The compiler's own manifest is the worked example — it bundles src/Runtime
directly, so it opts out of the runtime, and it has no other libraries to select:
{
"libraries": [{ "name": "stdlib", "src": "src/Runtime",
"output": "lib/manticore_stdlib.o", "exclude": [],
"runtime": true }],
"applications": [{ "name": "compiler", "src": "src", "output": "bin/manticore",
"entry": "src/main.php", "stdlib": false }]
}The stdlib library still builds — that is what user programs link.
Module interfaces (.sig)#
When a library builds, Manticore writes <output>.sig next to the .o — a
JSON table of the library's exported symbols (global functions, including
namespaced and FFI bindings). A dependent target imports the .sig to type
and resolve cross-unit calls without re-parsing the dependency's source.
This is what lets a distributed compiler ship only bin/ + lib/ (the
.o + .sig, no PHP sources) and still type-check and link user programs
against the bundled stdlib. You can inspect a .sig with:
manticore dump-sig src/Util/*.phpA .sig carries the library's classes, interfaces, enums and constants
alongside its functions (abi is MemoryAbi::VERSION at the time the library was built; a compiler refuses a .sig whose abi differs from its own):
{"schema":2,"abi":<MemoryAbi::VERSION>,
"functions":[…],
"classes":[{"name":"Acme\\Point","kind":"class","id":651526131271716,
"props":[…],"sprops":[…],"methods":[…],"consts":[…],
"layout":{"size":32,"hdr":16,"bag":-1,"offs":[["x",16],["y",24]],
"sum":4634270821217}}],
"constants":[{"n":"ACME_VERSION","v":{"k":"str","v":"1.0"}}],
"libs":[…],"weak":[…]}The dependent hydrates each entry into a declaration with no method bodies and
compiles it through the ordinary class pipeline, so its slot order, its bag
inheritance and its constant table are computed by the same code that built the
library's. The method symbols and the static-property cells stay in the
library's .o; the application declares them.
layout is a check, not information: the importer recomputes the offsets and
refuses on any disagreement. Every other way the two sides can drift produces a
wrong answer; drifting here writes past the end of an instance.
Class constants are const-folded at export (const B = self::A . '!' ships as
the finished string), so a dependent never has to evaluate anything. An
initializer that does not reduce to a literal, array or enum case is refused
when the library is built — where its author can fix it.
⚠ Still not exportable: traits (compile-time copy-paste — there is no
compiled form to link against, so a library's internal trait works but useing
one across the boundary does not), generic (@template) classes and
#[TypeDef]s. A generic or TypeDef is recorded in the .sig as
"unsupported" so the diagnostic can name the reason.
⚠ The bundled stdlib (runtime: true) exports its functions and only the php
classes it defines (HashContext, DeflateContext, InflateContext), which
every program imports. Its Runtime\… classes are internal and stdClass is
compiler-owned (every module registers it), so those stay unexported. The rest
of the class-shaped stdlib surface stays in prelude/.
Composer projects#
Set "composer": true on an application and Manticore builds the project the way
Composer sees it: the project's own composer.json autoload roots (psr-4,
psr-0 and classmap directories) and every installed package listed in
composer.lock, rooted at vendor/<name>/.
{ "applications": [{ "name": "app", "src": "src", "output": "bin/app",
"entry": "src/main.php", "composer": true }] }The object form narrows it: { "vendor": false } takes only the project's own
autoload — useful while a dependency uses PHP the compiler does not support yet.
The key point: vendor/ is source to compile, not an autoload map evaluated at
runtime. Those directories are unioned into the same whole-program source set
as src, and a directory already covered by src is skipped, so an
"App\\": "src/" mapping does not double-scan. The build prints each directory
it adds (build: + composer autoload '…').
How a user program gets the stdlib#
The stdlib is always on. Whether you compile a single file or build a
manifest, every program transparently has the bundled stdlib available —
str_pad, floor, array_reverse, json_decode, ctype_*,
file_get_contents, … — with zero manifest ceremony. You never list it.
manticore compile app.php -o app # str_pad() just works
manticore build manticore.json # …and so does every app targetHow it works: the compiler locates the bundled stdlib interface
lib/manticore_stdlib.o.sig (relative to the manticore binary, or via
MANTICORE_STDLIB_SIG / MANTICORE_STDLIB_O), injects externs for the stdlib
functions your code references, and links lib/manticore_stdlib.o at the final
step only when you actually call one — a program that touches no stdlib
function links nothing extra beyond the system libraries every binary carries
(libc, PCRE2, OpenSSL).
The stdlib is independent of the libraries selection: an app that depends
on specific user libraries ("libraries": ["mylib"]) still gets the stdlib. The
only way to opt out is an explicit "stdlib": false — which only the
self-contained compiler uses, because it already embeds src/Runtime and would
otherwise double-define it.
Note there are two tiers of standard functions. A core set (strlen, substr,
count, floor, sqrt, printf, …) are codegen builtins — emitted inline
as IR / LLVM intrinsics, always present, no link. The rest are PHP stdlib
(src/Runtime/Stdlib/*.php), compiled once into manticore_stdlib.o and reached
as above. Both are transparent to you — call the function, it works.
Building the compiler itself (bootstrap)#
The compiler is written in PHP and compiles itself. The bootstrap compiler is a
pinned release: BOOTSTRAP_VERSION at the repo root names it, and
tools/fetch_bootstrap.sh downloads it and checks it against the release's
SHA256SUMS. There is no Zend seed and no php is needed to build.
bin/build # no bin/manticore yet: fetch the pinned release, build the tree
# with it, then rebuild once with the result.
# Otherwise: the existing bin/manticore builds the manifest
# to a temp path, smoke-tests it, then atomically swaps in
bin/build --verify # rebuild, then run the fixpoint + suite gatebin/build ends by running build manticore.json — the manifest is the single
source of truth. The emitted binaries make no PHP-runtime calls — they link
against libc, PCRE2 and OpenSSL, plus any FFI-bound library the program names.
A feature may be used inside src/ only once BOOTSTRAP_VERSION names a release
that has it (the two-step rule, see AGENTS.md, "Bootstrap: the pinned release").
A memory-ABI bump needs no pin raise.
A self-rebuild is byte-identical: gen2 and gen3 emit the same IR.
Extensions (native libraries)#
An extension binds a native C library (zlib, libcurl, …) into your program:
thin PHP glue — an FFI binding plus a wrapper — compiled into the application,
and the native library linked by cc. The native lib never touches Manticore's
arena/rc heap, so it adds no runtime-corruption surface.
Declare extensions once under the manifest's top-level extensions, then opt
each application in by name:
{
"extensions": {
"zlib": { "src": "ext/zlib", "link": ["z"], "static": false }
},
"applications": [{
"name": "app", "src": "src/app", "output": "bin/app",
"entry": "src/app/main.php", "extensions": ["zlib"]
}]
}| Extension field | Meaning |
|---|---|
src | directory of the extension's PHP glue (*.php), compiled into every app that opts in |
link | native libraries to link — each becomes -l<name> (so "z" → -lz) |
static | reserved for static-archive linking (dynamic -l today) |
Opt-in is per application ("extensions": [...]), so a binary links libcurl
only if it actually uses the curl extension — small programs stay small.
Writing the glue#
The glue is ordinary PHP with FFI attributes. Bind the C symbol, then wrap it:
<?php
use Ffi\Library;
use Ffi\Symbol;
/** uLong crc32(uLong crc, const Bytef *buf, uInt len) */
#[Library('z'), Symbol('crc32')]
function __ffi_crc32(int $crc, string $buf, int $len): int { return 0; }
function ext_zlib_crc32(string $s): int {
return __ffi_crc32(0, $s, strlen($s));
}#[Symbol('crc32')] makes __ffi_crc32 a thin extern forwarder — its PHP body
is ignored; the compiler emits a direct call to the C symbol. Param/return
types map to the C ABI (string → ptr, int → i64, …). The full example
plus a build gate lives in ext/zlib* and tools/ext_smoke.sh. For the FFI
binding mechanism in full — type mapping, \Ffi\Ptr handles, the
body-is-ignored model, memory across the boundary — see docs/ffi.md.
Distributing a compiler#
A built compiler is self-contained in two directories:
bin/manticore the native compiler
lib/manticore_stdlib.o prebuilt stdlib object
lib/manticore_stdlib.o.sig its interfaceShip those — no PHP sources required. Users run
manticore compile their.php -o their and link the bundled stdlib by .sig
automatically.
Worked example#
myproj/
manticore.json
src/
main.php // <?php echo greet("world"), "\n";
Util/
greet.php // <?php function greet(string $n): string { return "hi $n"; }{
"libraries": [{ "name": "util", "src": "src/Util", "output": "lib/util.o", "exclude": [] }],
"applications": [{ "name": "app", "src": "src", "output": "bin/app",
"entry": "src/main.php", "exclude": ["src/Util"], "libraries": ["util"] }]
}manticore build # → lib/util.o (+ .sig) and bin/app
./bin/app # hi worldutil builds to a standalone object; app excludes src/Util from its own
scan, imports lib/util.o.sig to resolve greet, and links lib/util.o.