Menu

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 apps

build 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#

FieldMeaning
namelabel, shown in build output
srcdirectory scanned recursively for *.php
outputpath of the linked executable
entryoptional — 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).
excludeoptional — path prefixes to skip in the src scan (e.g. a subtree compiled separately as a library)
librariesoptional — which user library targets to import + link. Omit ⇒ all, [] ⇒ none, a named subset ⇒ just those. Independent of the stdlib (see below).
stdliboptional — set false to opt OUT of the always-on stdlib runtime. Only the self-contained compiler (which embeds src/Runtime) needs this.
composeroptional — true also compiles the Composer view of the project (see below); the object form { "vendor": false } narrows it
extensionsoptional — 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.

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/*.php

A .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 target

How 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 gate

bin/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 fieldMeaning
srcdirectory of the extension's PHP glue (*.php), compiled into every app that opts in
linknative libraries to link — each becomes -l<name> (so "z" → -lz)
staticreserved 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 interface

Ship 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 world

util 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.

Edit this page on GitHub