feat(compiler): add precompiled object file support and freestanding C++ ABI

- Add objects section to project.yml for linking precompiled .o/.obj files
- Implement getProjectObjectFiles method to retrieve precompiled objects
- Add c-flags and asm-flags support in CompilerBase
- Extend GCC backend to handle cflags and nativeflags configuration
- Update compiler to include project object files in linking process
- Add freestanding C++ allocation ABI implementation using Zend MM
- Include example TypePHP OS with kernel entry point and hardware stubs
- Add assembly files for boot sequence and kernel entry point
- Implement basic libc compatibility layer for Zend bootstrap
- Add try/catch macro replacement in generated code indentation
- Create bridge between C++ kernel and PHP code
- Add linker script for 64-bit kernel binary generation
master
韩天峰 4 weeks ago
parent 0fda2fe4ab
commit 1cad314678
  1. 13
      README-CN.md
  2. 15
      README.md
  3. 22
      docs/en/COMPILER_CLI.md
  4. 19
      docs/zh-cn/COMPILER_CLI.md
  5. 1
      examples/typephp-os/.gitignore
  6. 45
      examples/typephp-os/Makefile
  7. 118
      examples/typephp-os/README.md
  8. 49
      examples/typephp-os/ROADMAP.md
  9. 203
      examples/typephp-os/freestanding/abi/cxx.cpp
  10. 834
      examples/typephp-os/freestanding/abi/libc.c
  11. 38
      examples/typephp-os/freestanding/abi/typephp_os_abi.h
  12. 103
      examples/typephp-os/freestanding/abi/unimplemented.S
  13. 118
      examples/typephp-os/freestanding/boot.S
  14. 84
      examples/typephp-os/freestanding/bridge.cc
  15. 135
      examples/typephp-os/freestanding/kernel.c
  16. 31
      examples/typephp-os/freestanding/kernel64-entry.S
  17. 41
      examples/typephp-os/freestanding/kernel64.ld
  18. 49
      examples/typephp-os/freestanding/linker.ld
  19. 95
      examples/typephp-os/freestanding/memory.c
  20. 215
      examples/typephp-os/freestanding/time.c
  21. 57
      examples/typephp-os/freestanding/zend-allocator.c
  22. 57
      examples/typephp-os/project.yml
  23. 22
      examples/typephp-os/src/hardware.stub.php
  24. 110
      examples/typephp-os/src/kernel.php
  25. 38
      examples/typephp-os/tools/test-qemu.sh
  26. 16
      phpunit/src/Backend/BackendTest.php
  27. 31
      phpunit/src/CompilerBaseApiTest.php
  28. 6
      phpunit/src/GeneratedCodeIndentationTest.php
  29. 7
      src/Backend/GccLikeBackend.php
  30. 2
      src/Backend/Msvc.php
  31. 8
      src/Build/NativeCommandOptionsTrait.php
  32. 1
      src/Build/SourcePipelineTrait.php
  33. 4
      src/CompilerBase.php
  34. 18
      src/Config/ProjectYamlLoader.php
  35. 64
      src/Translator.php
  36. 5
      src/compiler.php

@ -304,6 +304,12 @@ sources:
- path: src/windows
if: PHP_OS_FAMILY == "Windows"
# 由项目自身的原生构建流程预编译。
objects:
- native/build/startup.o
- path: native/build/platform.obj
if: PHP_OS_FAMILY == "Windows"
ignore:
- src/experimental
@ -327,6 +333,13 @@ ext-deps:
`PHP_VERSION`、`PHP_VERSION_ID` 和 `PHP_OS_FAMILY`。命令行参数优先于 YAML
中的同名配置。原生链接依赖应写入 `link-libs`;`ext-deps` 会生成
`ZEND_MOD_REQUIRED`,缺少所需 PHP 扩展时由 Zend 拒绝加载模块。
通用的 `objects` 列表会把已有 `.o`/`.obj` 文件直接加入链接步骤。TypePHP
不会重新编译这些文件;原生编译器、目标架构、编译参数和增量构建均由项目负责。
使用目标通用参数的原生文件仍应放入 `sources`;仅当某个编译单元需要不同参数且
生成的对象与最终目标 ABI 兼容时,才应预编译后放入 `objects`。例如 `-m32`
生成的对象不能直接链接到 64 位目标,必须由项目在 tpc 产出 ELF 后另行封装。
项目级 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别应用于
C++、C、汇编和链接命令。
构建目录保存生成的 C++、依赖对象和预编译头缓存。复用同一个构建目录可以显著加快
增量构建;仅在确实需要重编 PHPX 公共对象时使用 `--force`。

@ -336,6 +336,12 @@ sources:
- path: src/windows
if: PHP_OS_FAMILY == "Windows"
# Precompiled by the project's external native build.
objects:
- native/build/startup.o
- path: native/build/platform.obj
if: PHP_OS_FAMILY == "Windows"
ignore:
- src/experimental
@ -360,6 +366,15 @@ directory; conditional entries support `PHP_VERSION`, `PHP_VERSION_ID`, and
`PHP_OS_FAMILY`. CLI arguments override their YAML counterparts. Native linker
dependencies belong in `link-libs`; `ext-deps` writes `ZEND_MOD_REQUIRED`
entries so Zend can reject loading when a required PHP extension is missing.
The generic `objects` list adds existing `.o`/`.obj` files directly to the
link step. TypePHP never recompiles these files; the project owns their native
compiler, architecture, flags, and incremental build. Keep native files that
use the common target options in `sources`; use `objects` for separately built
translation units that remain ABI-compatible with the final target. A `-m32`
object cannot be linked into a 64-bit target and requires a project-owned
post-link packaging step after tpc emits its ELF.
Project-wide `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` are applied to
C++, C, assembler, and link commands respectively.
The build directory contains generated C++, dependency objects, and the
precompiled-header cache. Reusing it makes incremental builds much faster;

@ -117,6 +117,28 @@ Corresponding long options:
When a `project.yml` is passed, command-line arguments take precedence over same-named settings in the YAML. For the project file format, see the user documentation and the project configuration parser in the code.
### Precompiled object files
A project can add object files produced by an external native toolchain as
generic link inputs:
```yaml
objects:
- build/startup.o
- path: build/platform.obj
if: PHP_OS_FAMILY == "Windows"
```
Paths are resolved relative to `project.yml` and accept the same conditions as
`sources`. `tpc` only loads and links `.o`/`.obj` files; it does not recompile
their C, C++, or assembly sources. Native sources using the project's common
options should remain in `sources`; `objects` is intended for separately built
translation units that remain ABI-compatible with the final target. Objects
built with `-m32` cannot be linked into a 64-bit target and need a separate
project-owned packaging step after tpc emits its ELF.
Use `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` for project-wide C++,
C, assembler, and linker options.
### PHP Extension Dependencies
When a program depends on other PHP extensions, the required modules can be written into the Zend module dependency table:

@ -117,6 +117,25 @@ TypePHP 和 PHPX 的最低运行时版本均为 PHP 8.4。`--php-version` 与实
传入 `project.yml` 时,命令行参数优先于 YAML 中的同名配置。项目文件格式参见用户文档及代码中的项目配置解析器。
### 预编译对象文件
项目可以把外部工具链生成的对象文件作为通用链接输入:
```yaml
objects:
- build/startup.o
- path: build/platform.obj
if: PHP_OS_FAMILY == "Windows"
```
路径相对于 `project.yml` 解析,并支持与 `sources` 相同的条件表达式。
`tpc` 只负责装载并链接 `.o`/`.obj`,不会重新编译其对应的 C、C++ 或汇编源码。
使用项目通用编译参数的原生源码应继续放在 `sources`;`objects` 主要用于单独编译
且与最终目标 ABI 兼容的编译单元。`-m32` 对象不能直接链接到 64 位目标,需要在
tpc 产出 ELF 后由项目自己的构建流程另行封装。
使用 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别设置项目级
C++、C、汇编和链接参数。
### PHP 扩展依赖
程序依赖其他 PHP 扩展时,可以将必需模块写入 Zend 模块依赖表:

@ -0,0 +1 @@
/build/

@ -0,0 +1,45 @@
BUILD := build
OBJECT_DIR := $(BUILD)/objects
BOOTSTRAP_OBJECT := $(OBJECT_DIR)/bootstrap.o
PAYLOAD_ELF := $(BUILD)/kernel64.elf
PAYLOAD_BIN := $(BUILD)/kernel64.bin
PAYLOAD_OBJECT := $(OBJECT_DIR)/kernel64-payload.o
KERNEL := $(BUILD)/typephp-os.elf
BOOTFLAGS := -m32 -O2 -ffreestanding -fno-pic -fno-pie \
-fno-stack-protector -Wall -Wextra
.PHONY: all payload objects run test clean
all: $(KERNEL)
payload: objects
cd ../.. && ./bin/tpc.php --nano examples/typephp-os/project.yml
# Only the 32-bit bootstrap requires flags incompatible with the 64-bit Nano
# payload. Ordinary .c/.cc/.S files stay in project.yml sources.
objects: $(BOOTSTRAP_OBJECT)
$(OBJECT_DIR):
mkdir -p $@
$(BOOTSTRAP_OBJECT): freestanding/boot.S | $(OBJECT_DIR)
gcc $(BOOTFLAGS) -c $< -o $@
$(KERNEL): payload
objcopy -O binary $(PAYLOAD_ELF) $(PAYLOAD_BIN)
objcopy -I binary -O elf32-i386 -B i386 \
--rename-section .data=.payload,alloc,load,readonly,data,contents \
$(PAYLOAD_BIN) $(PAYLOAD_OBJECT)
ld -m elf_i386 -T freestanding/linker.ld -o $@ \
$(BOOTSTRAP_OBJECT) $(PAYLOAD_OBJECT)
run: all
qemu-system-x86_64 -m 128M -kernel $(KERNEL) \
-display none -serial stdio -monitor none -no-reboot -no-shutdown
test: all
bash tools/test-qemu.sh $(KERNEL) $(BUILD)/qemu.log
clean:
rm -rf $(BUILD)

@ -0,0 +1,118 @@
# TypePHP freestanding OS experiment
This is a long-running experimental x86_64 TypePHP operating system. It boots
under QEMU and runs an ordinary TypePHP Nano program without linking hosted
PHP, libc, or libstdc++. TypePHP implements the startup self-check, a custom
Zend class, the prime-number demo, and the resident application loop. Small C
and assembly layers provide the machine bootstrap and current kernel services.
The architectural rules and staged plan are maintained in
[ROADMAP.md](ROADMAP.md).
## Architecture
There is no kernel-specific tpc mode or reduced PHP Nano profile. The 64-bit
payload is compiled with the normal command:
```shell
./bin/tpc.php --nano examples/typephp-os/project.yml
```
This composes the complete php-nano and PHPX source manifests. The TypePHP OS
project owns the freestanding boundary under `freestanding/abi`: implemented
C/POSIX and C++ ABI functions live there, while APIs required for linking but
not implemented by the kernel are exported as panic stubs. Consequently an
unsupported operation fails immediately with its ABI symbol instead of
silently returning fabricated data. Filesystem APIs are the next subsystem to
replace with real implementations; sockets are outside the current scope.
Cross-project portability uses general feature switches only:
- `PHP_NANO_NO_LIBC` asks the embedding host to provide Nano's clock, sleep,
and entropy hooks;
- `PHPX_NO_EXCEPTION` routes PHPX exception propagation to the host abort hook;
- `PHPX_NO_RTTI` selects PHPX's non-RTTI checked cast policy;
- `TYPEPHP_NO_MAIN` lets an embedding host provide the process/kernel entry.
None of these switches refers to TypePHP OS or to a kernel build. Ordinary
Nano and PHPX builds preserve their hosted defaults.
The payload uses PHP's original Zend allocator, GC, strings, HashTables,
objects, classes, exceptions, and built-in extension registration. Generated
projects and built-ins are registered through their ordinary MINIT paths; the
only excluded capability is dynamic PHP execution through ZendVM.
## Build and run
Required host tools are TypePHP's PHP/Composer dependencies, GCC/G++, GNU
binutils, GNU make, and `qemu-system-x86_64`.
From this directory, build and boot with:
```shell
make
make run
```
The build produces two useful files:
- `build/kernel64.elf`: the 64-bit TypePHP + ordinary Nano payload produced by
tpc;
- `build/typephp-os.elf`: the final Multiboot kernel accepted by QEMU.
Run the automated serial-output smoke test with:
```shell
make test
```
The Makefile compiles only the 32-bit Multiboot bootstrap externally because
its `-m32` ABI cannot participate in the 64-bit payload link. All ordinary
64-bit `.c`, `.cc`, and `.S` files remain in `project.yml` and use tpc's generic
`c-flags`, `cxx-flags`, and `asm-flags`. Generic same-ABI prebuilt objects can
be supplied with `objects`; the architecture-changing bootstrap is instead
combined during the final packaging link.
After tpc emits `kernel64.elf`, the Makefile uses `objcopy` to turn the payload
into a raw binary and then an ELF32 data object. GNU ld combines that object
with the 32-bit bootstrap. The bootstrap is loaded through Multiboot v1,
identity-maps the first GiB, enters x86_64 long mode, and transfers control to
the payload linked at 2 MiB.
To invoke QEMU manually:
```shell
qemu-system-x86_64 -m 128M \
-kernel examples/typephp-os/build/typephp-os.elf \
-display none -serial stdio -monitor none -no-reboot -no-shutdown
```
Press `Ctrl+A`, then `X`, to leave headless QEMU.
## Current capabilities
The bootstrap passes the Multiboot memory map to the 64-bit kernel. The
physical layer reserves the complete kernel image, selects mapped usable RAM,
and installs the remaining arena behind the project-owned `posix_memalign()`.
Upstream `zend_alloc` then obtains and subdivides aligned 2 MiB chunks. C++
global `new` and `delete`, including `std::vector` allocations, use Zend MM.
The QEMU smoke test currently verifies:
- 64-bit TypePHP scalar and control-flow execution;
- real Zend allocation, GC, string, array, object, class, and exception data;
- PHPX `Variant`, `Str`, `Array`, custom classes, and a typed
`std::vector<int>`;
- built-in date handling and `sleep()` through Zend Bridge;
- a TypePHP prime calculation for 0–100;
- a resident loop that prints the RTC-derived UTC time and
`Hello TypePHP-OS!` every two seconds.
The 64-bit payload currently occupies about 7 MiB, so the first 16 MiB is
reserved before physical memory is handed to Zend MM. Returning entire Zend
chunks to a future physical page allocator remains later work.
Filesystem ABI entries already link as explicit panic stubs. The next
milestone will replace them with an in-kernel filesystem and make PHP's local
file stream operations real. Network sockets, dynamic module loading,
`include`/`require`/`eval`, and external process execution remain unavailable.

@ -0,0 +1,49 @@
# TypePHP OS roadmap
This directory is a long-running experiment. Every milestone must remain
bootable in QEMU and must not change ordinary TypePHP, PHPX, or PHP Nano
behavior.
## Architecture rules
- The project is an ordinary `tpc --nano` consumer. tpc has no kernel option,
and PHP Nano has no kernel source profile.
- PHP and Composer remain build-time tools; the kernel never links `libphp`.
- Keep copied php-src C/H files unchanged and compile the complete Nano source
manifest. TypePHP OS owns the required libc/POSIX and C++ ABI boundary.
- Missing ABI operations must be linkable panic stubs. Replace each stub with
a real kernel service before exposing the corresponding PHP capability.
- Cross-project changes must use generally useful portability contracts such
as `PHP_NANO_NO_LIBC`, `PHPX_NO_EXCEPTION`, and `PHPX_NO_RTTI`; product-specific
conditionals are not allowed in PHP Nano or PHPX.
- Ordinary same-ABI source files and flags belong in `project.yml`. Translation
units that require incompatible per-file options are built by Make and may
be linked through tpc's generic `objects` facility when their ABI matches.
Architecture packaging and ELF/binary conversion happen after tpc emits the
64-bit ELF.
- Every completed milestone must pass the serial-output QEMU smoke test and
leave the 64-bit payload with no undefined symbols.
## Milestones
1. **Boot and scalar AOT — complete.** Multiboot v1 bootstrap, x86_64 long
mode, VGA/COM1 output, TypePHP scalars, functions, and control flow.
2. **Memory and Zend containers — complete.** Physical arena, TypePHP OS ABI
layer, original `zend_alloc`, `zend_gc`, `zend_string`, `zend_hash`, zval
destruction, and generated TypePHP arrays/strings through PHPX.
3. **Zend object model — complete for the current demo.** Original object
store, class entries, inheritance, interfaces, handlers, PHP exception
classes, generated user classes, construction, properties, and destruction.
4. **Time service — complete.** CMOS wall clock, monotonic host hook, built-in
date formatting, and the standard extension's `sleep()` path.
5. **Filesystem — next.** Physical block device, a small filesystem, file
descriptors, directory/stat operations, and PHP's local file stream API.
6. **Native Class memory.** Exercise Wren GC through Zend MM and verify tracing
of PHPX fields under sustained allocation.
7. **Kernel services.** Interrupt-driven timer, keyboard, physical-page
reclamation, and a capability-oriented native API.
8. **Packaging and CI.** Automate the two-stage ELF32/ELF64 build and QEMU boot
smoke test in GitHub Actions.
Later architectures may provide different bootstraps and host ABI adapters.
Generated TypePHP code and PHPX values remain 64-bit on every target.

@ -0,0 +1,203 @@
/*
+----------------------------------------------------------------------+
| TypePHP OS |
+----------------------------------------------------------------------+
| Freestanding C++ allocation ABI backed by Zend MM. |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
*/
extern "C" {
#include <zend_alloc.h>
#include "typephp_os_abi.h"
}
#include <cstddef>
#include <cstdint>
#include <new>
extern "C" {
void *__dso_handle = &__dso_handle;
int __cxa_atexit(void (*destructor)(void *), void *argument, void *dso)
{
(void) destructor;
(void) argument;
(void) dso;
return 0;
}
}
namespace {
void *zend_allocate(std::size_t size)
{
void *memory = emalloc(size == 0 ? 1 : size);
if (memory == nullptr) {
typephp_os_panic("C++ allocation failed");
}
return memory;
}
void *zend_allocate_aligned(std::size_t size, std::size_t alignment)
{
if (alignment <= alignof(std::max_align_t)) {
return zend_allocate(size);
}
const std::size_t extra = alignment - 1 + sizeof(void *);
if (size > static_cast<std::size_t>(-1) - extra) {
typephp_os_panic("C++ aligned allocation overflow");
}
void *base = zend_allocate(size + extra);
const auto start = reinterpret_cast<std::uintptr_t>(base) + sizeof(void *);
const auto aligned = (start + alignment - 1) & ~(static_cast<std::uintptr_t>(alignment) - 1);
void **result = reinterpret_cast<void **>(aligned);
result[-1] = base;
return result;
}
void zend_free_aligned(void *memory, std::size_t alignment) noexcept
{
if (memory == nullptr) {
return;
}
if (alignment <= alignof(std::max_align_t)) {
efree(memory);
return;
}
efree(reinterpret_cast<void **>(memory)[-1]);
}
} // namespace
void *operator new(std::size_t size)
{
return zend_allocate(size);
}
void *operator new[](std::size_t size)
{
return zend_allocate(size);
}
void *operator new(std::size_t size, const std::nothrow_t &) noexcept
{
return zend_allocate(size);
}
void *operator new[](std::size_t size, const std::nothrow_t &) noexcept
{
return zend_allocate(size);
}
void operator delete(void *memory) noexcept
{
if (memory != nullptr) {
efree(memory);
}
}
void operator delete[](void *memory) noexcept
{
if (memory != nullptr) {
efree(memory);
}
}
void operator delete(void *memory, std::size_t) noexcept
{
::operator delete(memory);
}
void operator delete[](void *memory, std::size_t) noexcept
{
::operator delete[](memory);
}
void operator delete(void *memory, const std::nothrow_t &) noexcept
{
::operator delete(memory);
}
void operator delete[](void *memory, const std::nothrow_t &) noexcept
{
::operator delete[](memory);
}
void *operator new(std::size_t size, std::align_val_t alignment)
{
return zend_allocate_aligned(size, static_cast<std::size_t>(alignment));
}
void *operator new[](std::size_t size, std::align_val_t alignment)
{
return zend_allocate_aligned(size, static_cast<std::size_t>(alignment));
}
void *operator new(
std::size_t size, std::align_val_t alignment, const std::nothrow_t &) noexcept
{
return zend_allocate_aligned(size, static_cast<std::size_t>(alignment));
}
void *operator new[](
std::size_t size, std::align_val_t alignment, const std::nothrow_t &) noexcept
{
return zend_allocate_aligned(size, static_cast<std::size_t>(alignment));
}
void operator delete(void *memory, std::align_val_t alignment) noexcept
{
zend_free_aligned(memory, static_cast<std::size_t>(alignment));
}
void operator delete[](void *memory, std::align_val_t alignment) noexcept
{
zend_free_aligned(memory, static_cast<std::size_t>(alignment));
}
void operator delete(void *memory, std::size_t, std::align_val_t alignment) noexcept
{
zend_free_aligned(memory, static_cast<std::size_t>(alignment));
}
void operator delete[](void *memory, std::size_t, std::align_val_t alignment) noexcept
{
zend_free_aligned(memory, static_cast<std::size_t>(alignment));
}
void operator delete(
void *memory, std::align_val_t alignment, const std::nothrow_t &) noexcept
{
zend_free_aligned(memory, static_cast<std::size_t>(alignment));
}
void operator delete[](
void *memory, std::align_val_t alignment, const std::nothrow_t &) noexcept
{
zend_free_aligned(memory, static_cast<std::size_t>(alignment));
}
/* libstdc++'s header-only containers call these ABI hooks on impossible or
* allocation-failure paths. The kernel deliberately does not link the
* hosted libstdc++ runtime, so terminate through its panic boundary. */
namespace std {
const nothrow_t nothrow{};
[[noreturn]] void __throw_length_error(const char *)
{
typephp_os_panic("C++ container length error");
}
[[noreturn]] void __throw_bad_alloc()
{
typephp_os_panic("C++ allocation failed");
}
[[noreturn]] void __throw_bad_array_new_length()
{
typephp_os_panic("C++ array allocation failed");
}
} // namespace std

@ -0,0 +1,834 @@
/*
+----------------------------------------------------------------------+
| TypePHP OS |
+----------------------------------------------------------------------+
| Small freestanding C/POSIX compatibility layer for Zend bootstrap. |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
This is not a replacement for zend_alloc. It only supplies the host calls
used by zend_alloc to acquire its aligned 2 MiB chunks, plus the C memory
primitives emitted by C/C++ compilers. User allocations continue through
emalloc/efree and therefore retain the original Zend allocator semantics.
*/
#include "typephp_os_abi.h"
#include <stdarg.h>
#include <locale.h>
#include <setjmp.h>
#include <stddef.h>
#include <stdint.h>
#include <inttypes.h>
typedef struct typephp_os_block {
size_t size;
} typephp_os_block;
static uintptr_t arena_cursor;
static uintptr_t arena_end;
static int typephp_os_errno;
/* TypePHP OS has no hosted stdio object. These opaque values only satisfy
* upstream error paths; bytes are forwarded through the host write hook. */
void *stdin = (void *) 0;
void *stdout = (void *) 1;
void *stderr = (void *) 2;
size_t strlen(const char *string);
void *malloc(size_t size);
int posix_memalign(void **result, size_t alignment, size_t size);
int vsnprintf(char *buffer, size_t size, const char *format, va_list args);
int *__errno_location(void)
{
return &typephp_os_errno;
}
void __assert_fail(
const char *assertion,
const char *file,
unsigned int line,
const char *function)
{
(void) file;
(void) line;
(void) function;
typephp_os_panic(assertion);
}
static uintptr_t align_up(uintptr_t value, size_t alignment)
{
return (value + alignment - 1u) & ~((uintptr_t) alignment - 1u);
}
static int is_power_of_two(size_t value)
{
return value != 0 && (value & (value - 1u)) == 0;
}
void typephp_os_memory_init(void *address, size_t size)
{
const uintptr_t begin = (uintptr_t) address;
if (size > UINTPTR_MAX - begin) {
arena_cursor = 0;
arena_end = 0;
return;
}
arena_cursor = begin;
arena_end = begin + size;
}
size_t typephp_os_memory_available(void)
{
return arena_end >= arena_cursor ? (size_t) (arena_end - arena_cursor) : 0;
}
__attribute__((weak)) void typephp_os_write(const char *data, size_t size)
{
(void) data;
(void) size;
}
__attribute__((weak, noreturn)) void typephp_os_panic(const char *message)
{
typephp_os_write(message, strlen(message));
for (;;) {
__asm__ volatile("cli; hlt");
}
}
void phpx_no_exception_abort(const char *message)
{
typephp_os_panic(message);
}
int php_nano_host_random_bytes(void *bytes, size_t size)
{
static uint64_t state = UINT64_C(0x9e3779b97f4a7c15);
unsigned char *output = (unsigned char *) bytes;
while (size-- != 0) {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
*output++ = (unsigned char) state;
}
return 0;
}
uint64_t php_nano_host_random_seed(void)
{
uint64_t seed = 0;
(void) php_nano_host_random_bytes(&seed, sizeof(seed));
return seed;
}
void *memset(void *destination, int value, size_t size)
{
unsigned char *output = (unsigned char *) destination;
while (size-- != 0) {
*output++ = (unsigned char) value;
}
return destination;
}
void *memcpy(void *destination, const void *source, size_t size)
{
unsigned char *output = (unsigned char *) destination;
const unsigned char *input = (const unsigned char *) source;
while (size-- != 0) {
*output++ = *input++;
}
return destination;
}
void *memmove(void *destination, const void *source, size_t size)
{
unsigned char *output = (unsigned char *) destination;
const unsigned char *input = (const unsigned char *) source;
if (output <= input || output >= input + size) {
return memcpy(destination, source, size);
}
output += size;
input += size;
while (size-- != 0) {
*--output = *--input;
}
return destination;
}
int memcmp(const void *left, const void *right, size_t size)
{
const unsigned char *a = (const unsigned char *) left;
const unsigned char *b = (const unsigned char *) right;
while (size-- != 0) {
if (*a != *b) {
return *a < *b ? -1 : 1;
}
++a;
++b;
}
return 0;
}
size_t strlen(const char *string)
{
const char *end = string;
while (*end != '\0') {
++end;
}
return (size_t) (end - string);
}
char *strdup(const char *string)
{
const size_t size = strlen(string) + 1;
char *copy = (char *) malloc(size);
return copy ? (char *) memcpy(copy, string, size) : 0;
}
int strcmp(const char *left, const char *right)
{
while (*left != '\0' && *left == *right) {
++left;
++right;
}
return (unsigned char) *left - (unsigned char) *right;
}
int strncmp(const char *left, const char *right, size_t size)
{
while (size-- != 0) {
const unsigned char a = (unsigned char) *left++;
const unsigned char b = (unsigned char) *right++;
if (a != b) {
return (int) a - (int) b;
}
if (a == 0) {
return 0;
}
}
return 0;
}
void *memchr(const void *memory, int character, size_t size)
{
const unsigned char *cursor = (const unsigned char *) memory;
const unsigned char value = (unsigned char) character;
while (size-- != 0) {
if (*cursor == value) {
return (void *) cursor;
}
++cursor;
}
return 0;
}
char *strchr(const char *string, int character)
{
do {
if (*string == (char) character) {
return (char *) string;
}
} while (*string++ != '\0');
return 0;
}
char *strrchr(const char *string, int character)
{
const char *match = 0;
do {
if (*string == (char) character) {
match = string;
}
} while (*string++ != '\0');
return (char *) match;
}
char *strstr(const char *haystack, const char *needle)
{
const size_t needle_size = strlen(needle);
if (needle_size == 0) {
return (char *) haystack;
}
while (*haystack != '\0') {
if (*haystack == *needle && strncmp(haystack, needle, needle_size) == 0) {
return (char *) haystack;
}
++haystack;
}
return 0;
}
static unsigned char ascii_lower(unsigned char character)
{
return character >= 'A' && character <= 'Z'
? (unsigned char) (character + ('a' - 'A'))
: character;
}
int strcasecmp(const char *left, const char *right)
{
while (*left != '\0' && ascii_lower((unsigned char) *left) == ascii_lower((unsigned char) *right)) {
++left;
++right;
}
return (int) ascii_lower((unsigned char) *left) - (int) ascii_lower((unsigned char) *right);
}
int strncasecmp(const char *left, const char *right, size_t size)
{
while (size-- != 0) {
const unsigned char a = ascii_lower((unsigned char) *left++);
const unsigned char b = ascii_lower((unsigned char) *right++);
if (a != b) {
return (int) a - (int) b;
}
if (a == 0) {
return 0;
}
}
return 0;
}
int isascii(int character)
{
return (character & ~0x7f) == 0;
}
int isdigit(int character)
{
return character >= '0' && character <= '9';
}
int islower(int character)
{
return character >= 'a' && character <= 'z';
}
int isupper(int character)
{
return character >= 'A' && character <= 'Z';
}
int isalpha(int character)
{
return islower(character) || isupper(character);
}
int isalnum(int character)
{
return isalpha(character) || isdigit(character);
}
int isspace(int character)
{
return character == ' ' || (character >= '\t' && character <= '\r');
}
int isblank(int character)
{
return character == ' ' || character == '\t';
}
int iscntrl(int character)
{
return (character >= 0 && character < 0x20) || character == 0x7f;
}
int isgraph(int character)
{
return character > 0x20 && character < 0x7f;
}
int isprint(int character)
{
return character >= 0x20 && character < 0x7f;
}
int ispunct(int character)
{
return isgraph(character) && !isalnum(character);
}
size_t __ctype_get_mb_cur_max(void)
{
return 1;
}
int isxdigit(int character)
{
return isdigit(character)
|| (character >= 'a' && character <= 'f')
|| (character >= 'A' && character <= 'F');
}
int tolower(int character)
{
return isupper(character) ? character + ('a' - 'A') : character;
}
int toupper(int character)
{
return islower(character) ? character - ('a' - 'A') : character;
}
struct lconv *localeconv(void)
{
static struct lconv value = {
.decimal_point = ".",
.thousands_sep = "",
.grouping = "",
};
return &value;
}
char *setlocale(int category, const char *locale)
{
static char c_locale[] = "C";
(void) category;
if (locale == 0 || strcmp(locale, "C") == 0 || strcmp(locale, "POSIX") == 0) {
return c_locale;
}
return 0;
}
char *getcwd(char *buffer, size_t size)
{
if (buffer == 0 || size < 2) {
typephp_os_errno = 34; /* ERANGE */
return 0;
}
buffer[0] = '/';
buffer[1] = '\0';
return buffer;
}
char *getenv(const char *name)
{
/* The kernel process starts with an intentionally empty environment. */
(void) name;
return 0;
}
int strcoll(const char *left, const char *right)
{
return strcmp(left, right);
}
char *strcat(char *destination, const char *source)
{
char *result = destination;
destination += strlen(destination);
while ((*destination++ = *source++) != '\0') {
}
return result;
}
char *strpbrk(const char *string, const char *characters)
{
for (; *string != '\0'; ++string) {
if (strchr(characters, *string) != 0) {
return (char *) string;
}
}
return 0;
}
static int digit_value(int character)
{
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'z') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'Z') {
return character - 'A' + 10;
}
return -1;
}
unsigned long long strtoull(const char *string, char **end, int base)
{
while (isspace((unsigned char) *string)) {
++string;
}
if (*string == '+') {
++string;
}
if ((base == 0 || base == 16) && string[0] == '0'
&& (string[1] == 'x' || string[1] == 'X')) {
base = 16;
string += 2;
} else if (base == 0) {
base = string[0] == '0' ? 8 : 10;
}
const char *cursor = string;
unsigned long long value = 0;
int digit;
while ((digit = digit_value((unsigned char) *cursor)) >= 0 && digit < base) {
value = value * (unsigned int) base + (unsigned int) digit;
++cursor;
}
if (end != 0) {
*end = (char *) cursor;
}
return value;
}
long long strtoll(const char *string, char **end, int base)
{
while (isspace((unsigned char) *string)) {
++string;
}
const int negative = *string == '-';
if (*string == '-' || *string == '+') {
++string;
}
char *parsed_end;
const unsigned long long value = strtoull(string, &parsed_end, base);
if (end != 0) {
*end = parsed_end;
}
return negative ? -(long long) value : (long long) value;
}
long strtol(const char *string, char **end, int base)
{
return (long) strtoll(string, end, base);
}
int atoi(const char *string)
{
return (int) strtol(string, 0, 10);
}
long long atoll(const char *string)
{
return strtoll(string, 0, 10);
}
intmax_t imaxabs(intmax_t value)
{
return value < 0 ? -value : value;
}
double strtod(const char *string, char **end)
{
while (isspace((unsigned char) *string)) {
++string;
}
const int negative = *string == '-';
if (*string == '-' || *string == '+') {
++string;
}
double value = 0.0;
while (isdigit((unsigned char) *string)) {
value = value * 10.0 + (double) (*string++ - '0');
}
if (*string == '.') {
double place = 0.1;
++string;
while (isdigit((unsigned char) *string)) {
value += (double) (*string++ - '0') * place;
place *= 0.1;
}
}
if (end != 0) {
*end = (char *) string;
}
return negative ? -value : value;
}
void qsort(void *base, size_t count, size_t width, int (*compare)(const void *, const void *))
{
unsigned char *bytes = (unsigned char *) base;
for (size_t index = 1; index < count; ++index) {
size_t current = index;
while (current != 0
&& compare(bytes + (current - 1) * width, bytes + current * width) > 0) {
for (size_t byte = 0; byte < width; ++byte) {
unsigned char value = bytes[(current - 1) * width + byte];
bytes[(current - 1) * width + byte] = bytes[current * width + byte];
bytes[current * width + byte] = value;
}
--current;
}
}
}
int fflush(void *stream)
{
(void) stream;
return 0;
}
int setvbuf(void *stream, char *buffer, int mode, size_t size)
{
(void) stream;
(void) buffer;
(void) mode;
(void) size;
return 0;
}
int vprintf(const char *format, va_list args)
{
char buffer[1024];
const int written = vsnprintf(buffer, sizeof(buffer), format, args);
if (written > 0) {
typephp_os_write(buffer, (size_t) written < sizeof(buffer)
? (size_t) written : sizeof(buffer) - 1);
}
return written;
}
int printf(const char *format, ...)
{
va_list args;
va_start(args, format);
const int written = vprintf(format, args);
va_end(args);
return written;
}
int _setjmp(jmp_buf environment)
{
(void) environment;
return 0;
}
double pow(double base, double exponent)
{
long power = (long) exponent;
if ((double) power != exponent) {
return 0.0;
}
double result = 1.0;
unsigned long magnitude = power < 0 ? (unsigned long) (-power) : (unsigned long) power;
while (magnitude != 0) {
if ((magnitude & 1u) != 0) {
result *= base;
}
base *= base;
magnitude >>= 1u;
}
return power < 0 ? 1.0 / result : result;
}
double fmod(double value, double divisor)
{
if (divisor == 0.0) {
return 0.0 / 0.0;
}
double quotient = value / divisor;
if (quotient >= 0.0) {
quotient = (double) (unsigned long) quotient;
} else {
quotient = (double) (long) quotient;
}
return value - quotient * divisor;
}
double ceil(double value)
{
long integral = (long) value;
return value > (double) integral ? (double) integral + 1.0 : (double) integral;
}
double floor(double value)
{
long integral = (long) value;
return value < (double) integral ? (double) integral - 1.0 : (double) integral;
}
double trunc(double value)
{
return (double) (long) value;
}
double round(double value)
{
return value < 0.0 ? ceil(value - 0.5) : floor(value + 0.5);
}
double fabs(double value)
{
return value < 0.0 ? -value : value;
}
int abs(int value)
{
return value < 0 ? -value : value;
}
long long llabs(long long value)
{
return value < 0 ? -value : value;
}
int ap_php_vsnprintf(char *buffer, size_t size, const char *format, va_list args);
int vsnprintf(char *buffer, size_t size, const char *format, va_list args)
{
return ap_php_vsnprintf(buffer, size, format, args);
}
int snprintf(char *buffer, size_t size, const char *format, ...)
{
va_list args;
va_start(args, format);
const int result = ap_php_vsnprintf(buffer, size, format, args);
va_end(args);
return result;
}
int __snprintf_chk(char *buffer, size_t size, int flag, size_t buffer_size, const char *format, ...)
{
(void) flag;
(void) buffer_size;
va_list args;
va_start(args, format);
const int result = ap_php_vsnprintf(buffer, size, format, args);
va_end(args);
return result;
}
__attribute__((noreturn)) void __longjmp_chk(void *environment, int value)
{
(void) environment;
(void) value;
typephp_os_panic("zend_bailout");
}
__attribute__((noreturn)) void longjmp(jmp_buf environment, int value)
{
(void) environment;
(void) value;
typephp_os_panic("zend_bailout");
}
int fputs(const char *string, void *stream)
{
(void) stream;
typephp_os_write(string, strlen(string));
return 0;
}
int fputc(int character, void *stream)
{
(void) stream;
const char byte = (char) character;
typephp_os_write(&byte, 1);
return (unsigned char) byte;
}
size_t fwrite(const void *data, size_t size, size_t count, void *stream)
{
(void) stream;
if (size != 0 && count > SIZE_MAX / size) {
return 0;
}
typephp_os_write((const char *) data, size * count);
return count;
}
int fprintf(void *stream, const char *format, ...)
{
(void) stream;
/* Zend's allocator only uses this on fatal paths. Keep the implementation
* allocation-free; the complete formatter belongs to the standard layer. */
typephp_os_write(format, strlen(format));
return (int) strlen(format);
}
int __fprintf_chk(void *stream, int flag, const char *format, ...)
{
(void) flag;
return fprintf(stream, format);
}
void *malloc(size_t size)
{
void *result = 0;
if (posix_memalign(&result, 16, size) != 0) {
return 0;
}
return result;
}
void free(void *pointer)
{
/* The bootstrap arena is monotonic. Zend's own allocations are reclaimed
* by zend_mm; backing chunks remain reserved until the kernel exits. */
(void) pointer;
}
void *calloc(size_t count, size_t size)
{
if (count != 0 && size > SIZE_MAX / count) {
return 0;
}
const size_t bytes = count * size;
void *result = malloc(bytes);
if (result != 0) {
memset(result, 0, bytes);
}
return result;
}
void *realloc(void *pointer, size_t size)
{
if (pointer == 0) {
return malloc(size);
}
if (size == 0) {
return 0;
}
typephp_os_block *old = (typephp_os_block *) pointer - 1;
void *replacement = malloc(size);
if (replacement != 0) {
memcpy(replacement, pointer, old->size < size ? old->size : size);
}
return replacement;
}
int posix_memalign(void **result, size_t alignment, size_t size)
{
if (result == 0 || !is_power_of_two(alignment) || alignment < sizeof(void *)) {
return 22; /* EINVAL */
}
if (arena_cursor == 0 || size > SIZE_MAX - sizeof(typephp_os_block)) {
*result = 0;
return 12; /* ENOMEM */
}
const uintptr_t payload = align_up(
arena_cursor + sizeof(typephp_os_block), alignment);
if (payload > arena_end || size > arena_end - payload) {
*result = 0;
return 12;
}
typephp_os_block *block = (typephp_os_block *) payload - 1;
block->size = size;
arena_cursor = payload + size;
*result = (void *) payload;
return 0;
}
__attribute__((noreturn)) void abort(void)
{
typephp_os_panic("abort");
}
__attribute__((noreturn)) void exit(int status)
{
(void) status;
typephp_os_panic("exit");
}
__attribute__((noreturn)) void _Exit(int status)
{
exit(status);
}

@ -0,0 +1,38 @@
/*
+----------------------------------------------------------------------+
| TypePHP OS |
+----------------------------------------------------------------------+
| Freestanding host contract owned by the TypePHP OS experiment. |
| SPDX-License-Identifier: BSD-3-Clause |
+----------------------------------------------------------------------+
*/
#ifndef TYPEPHP_OS_ABI_H
#define TYPEPHP_OS_ABI_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Install the contiguous physical-memory arena used by the libc/POSIX shim.
* The caller owns page-table setup and must keep the whole region mapped. */
void typephp_os_memory_init(void *address, size_t size);
size_t typephp_os_memory_available(void);
/* C++17 global new/delete are supplied by TypePHP OS and allocate
* exclusively through Zend MM (emalloc/efree). */
/* A platform may override these weak hooks for diagnostics and shutdown. */
void typephp_os_write(const char *data, size_t size);
void typephp_os_panic(const char *message);
/* Generic PHPX no-exception policy hook. */
void phpx_no_exception_abort(const char *message);
#ifdef __cplusplus
}
#endif
#endif

@ -0,0 +1,103 @@
.section .text
.code64
.extern typephp_os_panic
/* ABI coverage comes before subsystem implementation. Every placeholder is a
* real exported symbol so the complete php-nano/PHPX source set links, while
* any accidental runtime use fails loudly instead of returning fake data. */
.macro TYPEPHP_OS_UNIMPLEMENTED symbol
.global \symbol
.type \symbol, @function
\symbol:
lea .Lmessage\@(%rip), %rdi
jmp typephp_os_panic
.pushsection .rodata
.Lmessage\@:
.asciz "unimplemented libc/POSIX ABI: \symbol"
.popsection
.endm
TYPEPHP_OS_UNIMPLEMENTED access
TYPEPHP_OS_UNIMPLEMENTED chdir
TYPEPHP_OS_UNIMPLEMENTED chmod
TYPEPHP_OS_UNIMPLEMENTED chown
TYPEPHP_OS_UNIMPLEMENTED close
TYPEPHP_OS_UNIMPLEMENTED closedir
TYPEPHP_OS_UNIMPLEMENTED fcntl
TYPEPHP_OS_UNIMPLEMENTED fdopen
TYPEPHP_OS_UNIMPLEMENTED feof
TYPEPHP_OS_UNIMPLEMENTED fileno
TYPEPHP_OS_UNIMPLEMENTED flock
TYPEPHP_OS_UNIMPLEMENTED fopen
TYPEPHP_OS_UNIMPLEMENTED fclose
TYPEPHP_OS_UNIMPLEMENTED fread
TYPEPHP_OS_UNIMPLEMENTED fseek
TYPEPHP_OS_UNIMPLEMENTED fstat
TYPEPHP_OS_UNIMPLEMENTED fsync
TYPEPHP_OS_UNIMPLEMENTED ftell
TYPEPHP_OS_UNIMPLEMENTED ftruncate
TYPEPHP_OS_UNIMPLEMENTED getgid
TYPEPHP_OS_UNIMPLEMENTED getgrnam
TYPEPHP_OS_UNIMPLEMENTED getgroups
TYPEPHP_OS_UNIMPLEMENTED getpwnam
TYPEPHP_OS_UNIMPLEMENTED getpwnam_r
TYPEPHP_OS_UNIMPLEMENTED getpwuid_r
TYPEPHP_OS_UNIMPLEMENTED getuid
TYPEPHP_OS_UNIMPLEMENTED gnu_dev_major
TYPEPHP_OS_UNIMPLEMENTED gnu_dev_minor
TYPEPHP_OS_UNIMPLEMENTED isatty
TYPEPHP_OS_UNIMPLEMENTED lseek
TYPEPHP_OS_UNIMPLEMENTED lstat
TYPEPHP_OS_UNIMPLEMENTED mkdir
TYPEPHP_OS_UNIMPLEMENTED mkstemp
TYPEPHP_OS_UNIMPLEMENTED open
TYPEPHP_OS_UNIMPLEMENTED opendir
TYPEPHP_OS_UNIMPLEMENTED read
TYPEPHP_OS_UNIMPLEMENTED readdir
TYPEPHP_OS_UNIMPLEMENTED readlink
TYPEPHP_OS_UNIMPLEMENTED rename
TYPEPHP_OS_UNIMPLEMENTED rewind
TYPEPHP_OS_UNIMPLEMENTED rewinddir
TYPEPHP_OS_UNIMPLEMENTED rmdir
TYPEPHP_OS_UNIMPLEMENTED stat
TYPEPHP_OS_UNIMPLEMENTED umask
TYPEPHP_OS_UNIMPLEMENTED unlink
TYPEPHP_OS_UNIMPLEMENTED utime
TYPEPHP_OS_UNIMPLEMENTED write
TYPEPHP_OS_UNIMPLEMENTED acos
TYPEPHP_OS_UNIMPLEMENTED acosh
TYPEPHP_OS_UNIMPLEMENTED asin
TYPEPHP_OS_UNIMPLEMENTED asinh
TYPEPHP_OS_UNIMPLEMENTED atan
TYPEPHP_OS_UNIMPLEMENTED atan2
TYPEPHP_OS_UNIMPLEMENTED atanh
TYPEPHP_OS_UNIMPLEMENTED copysign
TYPEPHP_OS_UNIMPLEMENTED cos
TYPEPHP_OS_UNIMPLEMENTED cosh
TYPEPHP_OS_UNIMPLEMENTED exp
TYPEPHP_OS_UNIMPLEMENTED expm1
TYPEPHP_OS_UNIMPLEMENTED hypot
TYPEPHP_OS_UNIMPLEMENTED log
TYPEPHP_OS_UNIMPLEMENTED log10
TYPEPHP_OS_UNIMPLEMENTED log1p
TYPEPHP_OS_UNIMPLEMENTED log2
TYPEPHP_OS_UNIMPLEMENTED nextafter
TYPEPHP_OS_UNIMPLEMENTED sin
TYPEPHP_OS_UNIMPLEMENTED sinh
TYPEPHP_OS_UNIMPLEMENTED sqrt
TYPEPHP_OS_UNIMPLEMENTED tan
TYPEPHP_OS_UNIMPLEMENTED tanh
TYPEPHP_OS_UNIMPLEMENTED mblen
TYPEPHP_OS_UNIMPLEMENTED strerror
TYPEPHP_OS_UNIMPLEMENTED strftime
TYPEPHP_OS_UNIMPLEMENTED _Unwind_Resume
TYPEPHP_OS_UNIMPLEMENTED __cxa_begin_catch
TYPEPHP_OS_UNIMPLEMENTED __cxa_end_catch
TYPEPHP_OS_UNIMPLEMENTED __cxa_rethrow
TYPEPHP_OS_UNIMPLEMENTED __gxx_personality_v0
TYPEPHP_OS_UNIMPLEMENTED _ZNSt15__exception_ptr13exception_ptr10_M_releaseEv
.section .note.GNU-stack, "", @progbits

@ -0,0 +1,118 @@
.set MULTIBOOT_MAGIC, 0x1badb002
.set MULTIBOOT_FLAGS, 0x00000003
.set MULTIBOOT_CHECKSUM, -(MULTIBOOT_MAGIC + MULTIBOOT_FLAGS)
.section .multiboot, "a"
.align 4
.long MULTIBOOT_MAGIC
.long MULTIBOOT_FLAGS
.long MULTIBOOT_CHECKSUM
.section .bss
.align 4096
pml4_table:
.skip 4096
pdp_table:
.skip 4096
pd_table:
.skip 4096
.align 16
stack_bottom:
.skip 16384
stack_top:
.section .text
.code32
.global _start
.type _start, @function
.extern kernel_entry
_start:
cli
mov $stack_top, %esp
# Preserve the Multiboot registers while page tables and MSRs are set up.
mov %eax, %ebp
# Identity-map the first GiB with 2 MiB pages.
mov $pdp_table, %eax
or $0x3, %eax
mov %eax, pml4_table
mov $pd_table, %eax
or $0x3, %eax
mov %eax, pdp_table
mov $pd_table, %edi
mov $0x83, %eax
mov $512, %ecx
map_page:
mov %eax, (%edi)
add $0x200000, %eax
add $8, %edi
loop map_page
# Enable PAE and prepare the x87/SSE state before entering long mode.
mov %cr0, %ecx
and $0xfffffffb, %ecx
or $0x2, %ecx
mov %ecx, %cr0
mov %cr4, %ecx
or $0x620, %ecx
mov %ecx, %cr4
fninit
mov $pml4_table, %eax
mov %eax, %cr3
# Set EFER.LME, then enable paging. Protected mode is already active.
mov $0xc0000080, %ecx
rdmsr
or $0x100, %eax
wrmsr
mov %cr0, %eax
or $0x80000000, %eax
mov %eax, %cr0
lgdt gdt64_pointer
# The 64-bit System V ABI receives Multiboot magic/info in RDI/RSI.
mov %ebp, %edi
mov %ebx, %esi
ljmp $0x08, $long_mode_start
.code64
long_mode_start:
mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %ss
xor %ax, %ax
mov %ax, %fs
mov %ax, %gs
and $-16, %rsp
# The embedded ELF64 payload is linked and loaded at exactly 2 MiB.
movabs $0x200000, %rax
call *%rax
halt_forever:
cli
hlt
jmp halt_forever
.size _start, . - _start
.section .rodata
.align 8
gdt64:
.quad 0x0000000000000000
.quad 0x00af9a000000ffff
.quad 0x00cf92000000ffff
gdt64_end:
gdt64_pointer:
.word gdt64_end - gdt64 - 1
.long gdt64
.section .note.GNU-stack, "", @progbits

@ -0,0 +1,84 @@
#include <typephp_os_abi.h>
#include <php_nano_extension.h>
#include <phpx.h>
#include <phpx_helper.h>
void php_main();
extern "C" {
#include <zend_exceptions.h>
}
extern "C" void kernel_clear_c(int color);
extern "C" void kernel_put_char_c(int ascii, int color);
extern "C" void kernel_halt_c();
extern "C" unsigned long physical_memory_megabytes();
extern "C" unsigned long physical_chunk_smoke_test();
static void exception_model_smoke_test()
{
php::enableDebugInfo(true);
php::pushDebugFrame("kernel-smoke.php", 42, "Kernel::smoke");
zend_throw_exception_ex(zend_ce_type_error, 0, "%s", "kernel exception");
if (EG(exception) == nullptr || EG(exception)->ce != zend_ce_type_error) {
typephp_os_panic("Zend TypeError construction failed");
}
php::augmentException();
zval value;
zval *file = zend_read_property_ex(
zend_get_exception_base(EG(exception)), EG(exception), ZSTR_KNOWN(ZEND_STR_FILE), true, &value);
if (Z_TYPE_P(file) != IS_STRING || !zend_string_equals_literal(Z_STR_P(file), "kernel-smoke.php")) {
typephp_os_panic("PHPX exception augmentation failed");
}
zend_clear_exception();
php::popDebugFrame();
php::enableDebugInfo(false);
}
extern "C" void typephp_kernel_main()
{
if (php_nano_startup_composer_extensions() != SUCCESS) {
typephp_os_panic("Unable to start Nano extensions");
}
exception_model_smoke_test();
php_main();
}
void php_kernel_clear(php::Int color)
{
kernel_clear_c(static_cast<int>(color));
}
void php_kernel_put_char(php::Int ascii, php::Int color)
{
kernel_put_char_c(static_cast<int>(ascii), static_cast<int>(color));
}
void php_kernel_write(php::Str text, php::Int color)
{
for (size_t i = 0; i < text.length(); ++i) {
kernel_put_char_c(static_cast<unsigned char>(text.data()[i]), static_cast<int>(color));
}
}
php::Int php_kernel_word_bits()
{
return static_cast<php::Int>(sizeof(php::Int) * 8);
}
php::Int php_kernel_memory_megabytes()
{
return static_cast<php::Int>(physical_memory_megabytes());
}
php::Int php_kernel_chunk_smoke_test()
{
return static_cast<php::Int>(physical_chunk_smoke_test());
}
void php_kernel_halt()
{
kernel_halt_c();
}

@ -0,0 +1,135 @@
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long uint64_t;
enum {
MULTIBOOT_BOOTLOADER_MAGIC = 0x2badb002u,
VGA_WIDTH = 80,
VGA_HEIGHT = 25,
COM1 = 0x3f8,
};
static volatile uint16_t *const vga = (volatile uint16_t *) 0xb8000;
static uint32_t row;
static uint32_t column;
static inline void outb(uint16_t port, uint8_t value)
{
__asm__ volatile("outb %0, %1" : : "a"(value), "Nd"(port));
}
static inline uint8_t inb(uint16_t port)
{
uint8_t value;
__asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port));
return value;
}
static void serial_init(void)
{
outb(COM1 + 1, 0x00);
outb(COM1 + 3, 0x80);
outb(COM1 + 0, 0x03);
outb(COM1 + 1, 0x00);
outb(COM1 + 3, 0x03);
outb(COM1 + 2, 0xc7);
outb(COM1 + 4, 0x0b);
}
static void serial_put(uint8_t value)
{
while ((inb(COM1 + 5) & 0x20u) == 0) {
}
outb(COM1, value);
}
static void vga_scroll(void)
{
if (row < VGA_HEIGHT) {
return;
}
for (uint32_t y = 1; y < VGA_HEIGHT; y++) {
for (uint32_t x = 0; x < VGA_WIDTH; x++) {
vga[(y - 1) * VGA_WIDTH + x] = vga[y * VGA_WIDTH + x];
}
}
for (uint32_t x = 0; x < VGA_WIDTH; x++) {
vga[(VGA_HEIGHT - 1) * VGA_WIDTH + x] = (uint16_t) (0x0720u);
}
row = VGA_HEIGHT - 1;
}
void kernel_clear_c(int color)
{
const uint16_t blank = (uint16_t) ((((uint16_t) color) << 8u) | ' ');
for (uint32_t i = 0; i < VGA_WIDTH * VGA_HEIGHT; i++) {
vga[i] = blank;
}
row = 0;
column = 0;
}
void kernel_put_char_c(int ascii, int color)
{
const uint8_t character = (uint8_t) ascii;
if (character == '\n') {
serial_put('\r');
serial_put('\n');
column = 0;
row++;
vga_scroll();
return;
}
if (character == '\t') {
do {
kernel_put_char_c(' ', color);
} while ((column & 7u) != 0);
return;
}
serial_put(character);
vga[row * VGA_WIDTH + column] =
(uint16_t) ((((uint16_t) color) << 8u) | character);
column++;
if (column == VGA_WIDTH) {
column = 0;
row++;
vga_scroll();
}
}
void typephp_os_write(const char *data, unsigned long size)
{
for (unsigned long index = 0; index < size; ++index) {
kernel_put_char_c((unsigned char) data[index], 0x0f);
}
}
void kernel_halt_c(void)
{
// QEMU's isa-debug-exit device turns this into process exit status 33.
outb(0xf4, 0x10);
for (;;) {
__asm__ volatile("cli; hlt");
}
}
extern void typephp_kernel_main(void);
extern void physical_memory_init(uint64_t multiboot_info_address);
void kernel_entry(uint64_t magic, uint64_t multiboot_info)
{
(void) multiboot_info;
serial_init();
if (magic != MULTIBOOT_BOOTLOADER_MAGIC) {
const char *message = "Invalid Multiboot boot magic\n";
while (*message != '\0') {
serial_put((uint8_t) *message++);
}
kernel_halt_c();
}
physical_memory_init(multiboot_info);
typephp_kernel_main();
kernel_halt_c();
}

@ -0,0 +1,31 @@
.section .text.boot, "ax"
.code64
.global kernel64_entry
.type kernel64_entry, @function
.extern kernel_entry
.extern __bss_start
.extern __bss_end
kernel64_entry:
cld
mov %rdi, %r12
mov %rsi, %r13
# The raw payload does not carry ELF NOBITS data, so clear it explicitly.
lea __bss_start(%rip), %rdi
lea __bss_end(%rip), %rcx
sub %rdi, %rcx
xor %eax, %eax
rep stosb
mov %r12, %rdi
mov %r13, %rsi
call kernel_entry
halt64:
cli
hlt
jmp halt64
.size kernel64_entry, . - kernel64_entry
.section .note.GNU-stack, "", @progbits

@ -0,0 +1,41 @@
OUTPUT_FORMAT(elf64-x86-64)
OUTPUT_ARCH(i386:x86-64)
ENTRY(kernel64_entry)
SECTIONS
{
. = 2M;
.text ALIGN(4K) :
{
KEEP(*(.text.boot))
*(.text .text.*)
}
.rodata ALIGN(4K) :
{
*(.rodata .rodata.*)
}
.data ALIGN(4K) :
{
*(.data .data.*)
}
.bss ALIGN(4K) (NOLOAD) :
{
__bss_start = .;
*(COMMON)
*(.bss .bss.*)
__bss_end = .;
}
ASSERT(__bss_end <= 16M, "64-bit kernel exceeds its reserved boot memory")
/DISCARD/ :
{
*(.comment)
*(.eh_frame*)
*(.note*)
}
}

@ -0,0 +1,49 @@
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
ENTRY(_start)
SECTIONS
{
. = 1M;
.multiboot ALIGN(4) :
{
KEEP(*(.multiboot))
}
.text ALIGN(4K) :
{
*(.text .text.*)
}
.rodata ALIGN(4K) :
{
*(.rodata .rodata.*)
}
.data ALIGN(4K) :
{
*(.data .data.*)
}
.bss ALIGN(4K) (NOLOAD) :
{
*(COMMON)
*(.bss .bss.*)
}
ASSERT(. < 2M, "32-bit bootstrap overlaps the 64-bit payload")
. = 2M;
.payload ALIGN(4K) :
{
KEEP(*(.payload))
}
/DISCARD/ :
{
*(.comment)
*(.eh_frame*)
*(.note*)
}
}

@ -0,0 +1,95 @@
typedef unsigned int uint32_t;
typedef unsigned long uint64_t;
extern void typephp_os_memory_init(void *address, unsigned long size);
enum {
MULTIBOOT_INFO_MEMORY_MAP = 1u << 6,
};
static const uint64_t CHUNK_SIZE = 2ul * 1024ul * 1024ul;
/* Keep the complete TypePHP + PHP Nano payload away from the page allocator. */
static const uint64_t KERNEL_RESERVED_END = 16ul * 1024ul * 1024ul;
static const uint64_t IDENTITY_MAP_END = 1024ul * 1024ul * 1024ul;
typedef struct __attribute__((packed)) {
uint32_t size;
uint64_t address;
uint64_t length;
uint32_t type;
} multiboot_memory_entry;
typedef struct __attribute__((packed)) {
uint32_t flags;
uint32_t memory_lower;
uint32_t memory_upper;
uint32_t boot_device;
uint32_t command_line;
uint32_t modules_count;
uint32_t modules_address;
uint32_t symbols[4];
uint32_t memory_map_length;
uint32_t memory_map_address;
} multiboot_info;
static uint64_t usable_memory_bytes;
static uint64_t chunk_cursor;
static uint64_t chunk_end;
static uint64_t align_up(uint64_t value, uint64_t alignment)
{
return (value + alignment - 1) & ~(alignment - 1);
}
void physical_memory_init(uint64_t multiboot_info_address)
{
const multiboot_info *info =
(const multiboot_info *) (unsigned long) multiboot_info_address;
usable_memory_bytes = 0;
chunk_cursor = 0;
chunk_end = 0;
if ((info->flags & MULTIBOOT_INFO_MEMORY_MAP) == 0) {
return;
}
uint64_t position = info->memory_map_address;
const uint64_t end = position + info->memory_map_length;
while (position + sizeof(multiboot_memory_entry) <= end) {
const multiboot_memory_entry *entry =
(const multiboot_memory_entry *) (unsigned long) position;
if (entry->size < sizeof(multiboot_memory_entry) - sizeof(uint32_t)) {
break;
}
if (entry->type == 1) {
usable_memory_bytes += entry->length;
uint64_t region_end = entry->address + entry->length;
if (region_end > IDENTITY_MAP_END) {
region_end = IDENTITY_MAP_END;
}
uint64_t region_start = entry->address;
if (region_start < KERNEL_RESERVED_END) {
region_start = KERNEL_RESERVED_END;
}
region_start = align_up(region_start, CHUNK_SIZE);
if (region_start + CHUNK_SIZE <= region_end
&& region_end - region_start > chunk_end - chunk_cursor) {
chunk_cursor = region_start;
chunk_end = region_end;
}
}
position += (uint64_t) entry->size + sizeof(entry->size);
}
if (chunk_cursor != 0 && chunk_end > chunk_cursor) {
typephp_os_memory_init(
(void *) (unsigned long) chunk_cursor,
(unsigned long) (chunk_end - chunk_cursor));
}
}
uint64_t physical_memory_megabytes(void)
{
return usable_memory_bytes >> 20;
}

@ -0,0 +1,215 @@
#include <php.h>
enum {
CMOS_ADDRESS = 0x70,
CMOS_DATA = 0x71,
};
typedef struct kernel_datetime {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
} kernel_datetime;
static inline void outb(uint16_t port, uint8_t value)
{
__asm__ volatile("outb %0, %1" : : "a"(value), "Nd"(port));
}
static inline uint8_t inb(uint16_t port)
{
uint8_t value;
__asm__ volatile("inb %1, %0" : "=a"(value) : "Nd"(port));
return value;
}
static uint8_t cmos_read(uint8_t index)
{
/* Keep NMI disabled while selecting a CMOS register. The kernel does not
* install an IDT yet, so accepting an NMI here would be unsafe. */
outb(CMOS_ADDRESS, (uint8_t) (index | 0x80u));
return inb(CMOS_DATA);
}
static int rtc_update_in_progress(void)
{
return (cmos_read(0x0a) & 0x80u) != 0;
}
static uint8_t bcd_to_binary(uint8_t value)
{
return (uint8_t) ((value & 0x0fu) + ((value >> 4u) * 10u));
}
static void rtc_snapshot(kernel_datetime *value)
{
uint8_t second;
uint8_t minute;
uint8_t hour;
uint8_t day;
uint8_t month;
uint8_t year;
uint8_t century;
uint8_t status_b;
uint8_t previous_second;
uint8_t previous_minute;
uint8_t previous_hour;
uint8_t previous_day;
uint8_t previous_month;
uint8_t previous_year;
uint8_t previous_century;
do {
while (rtc_update_in_progress()) {
__asm__ volatile("pause");
}
previous_second = cmos_read(0x00);
previous_minute = cmos_read(0x02);
previous_hour = cmos_read(0x04);
previous_day = cmos_read(0x07);
previous_month = cmos_read(0x08);
previous_year = cmos_read(0x09);
previous_century = cmos_read(0x32);
while (rtc_update_in_progress()) {
__asm__ volatile("pause");
}
second = cmos_read(0x00);
minute = cmos_read(0x02);
hour = cmos_read(0x04);
day = cmos_read(0x07);
month = cmos_read(0x08);
year = cmos_read(0x09);
century = cmos_read(0x32);
} while (second != previous_second || minute != previous_minute
|| hour != previous_hour || day != previous_day
|| month != previous_month || year != previous_year
|| century != previous_century);
status_b = cmos_read(0x0b);
if ((status_b & 0x04u) == 0) {
second = bcd_to_binary(second);
minute = bcd_to_binary(minute);
day = bcd_to_binary(day);
month = bcd_to_binary(month);
year = bcd_to_binary(year);
century = bcd_to_binary(century);
hour = (uint8_t) ((hour & 0x80u) | bcd_to_binary((uint8_t) (hour & 0x7fu)));
}
if ((status_b & 0x02u) == 0 && (hour & 0x80u) != 0) {
hour = (uint8_t) (((hour & 0x7fu) + 12u) % 24u);
} else {
hour &= 0x7fu;
}
value->year = century >= 19 && century <= 99
? (uint16_t) (century * 100u + year)
: (uint16_t) (2000u + year);
value->month = month;
value->day = day;
value->hour = hour;
value->minute = minute;
value->second = second;
}
static int is_leap_year(uint64_t year)
{
return (year % 4u == 0 && year % 100u != 0) || year % 400u == 0;
}
static uint64_t rtc_seconds(const kernel_datetime *value)
{
static const uint16_t days_before_month[] = {
0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
};
uint64_t year = value->year;
uint64_t previous_year = year - 1u;
uint64_t days = 365u * (year - 1970u)
+ previous_year / 4u - 1969u / 4u
- previous_year / 100u + 1969u / 100u
+ previous_year / 400u - 1969u / 400u;
days += days_before_month[value->month];
if (value->month > 2 && is_leap_year(year)) {
++days;
}
days += value->day - 1u;
return (((days * 24u + value->hour) * 60u + value->minute) * 60u)
+ value->second;
}
void kernel_datetime_c(kernel_datetime *value)
{
rtc_snapshot(value);
}
void kernel_sleep_c(uint64_t seconds)
{
kernel_datetime now;
rtc_snapshot(&now);
const uint64_t deadline = rtc_seconds(&now) + seconds;
do {
__asm__ volatile("pause");
rtc_snapshot(&now);
} while (rtc_seconds(&now) < deadline);
}
long typephp_os_time_seconds(void)
{
kernel_datetime now;
rtc_snapshot(&now);
return (long) rtc_seconds(&now);
}
void php_nano_host_system_time(int64_t *seconds, int32_t *microseconds)
{
*seconds = (int64_t) typephp_os_time_seconds();
*microseconds = 0;
}
uint64_t php_nano_host_monotonic_nanoseconds(void)
{
return (uint64_t) typephp_os_time_seconds() * UINT64_C(1000000000);
}
void php_nano_host_sleep(uint64_t seconds, uint32_t nanoseconds)
{
if (nanoseconds != 0) {
/* RTC resolution is currently one second. Round sub-second sleeps up
* until the timer interrupt implementation is available. */
++seconds;
}
kernel_sleep_c(seconds);
}
unsigned int sleep(unsigned int seconds)
{
kernel_sleep_c(seconds);
return 0;
}
time_t time(time_t *result)
{
const time_t value = (time_t) typephp_os_time_seconds();
if (result != 0) {
*result = value;
}
return value;
}
int clock_gettime(clockid_t clock_id, struct timespec *value)
{
(void) clock_id;
value->tv_sec = (time_t) typephp_os_time_seconds();
value->tv_nsec = 0;
return 0;
}
int nanosleep(const struct timespec *duration, struct timespec *remaining)
{
(void) remaining;
php_nano_host_sleep((uint64_t) duration->tv_sec, (uint32_t) duration->tv_nsec);
return 0;
}

@ -0,0 +1,57 @@
#include <php.h>
#include <zend_gc.h>
unsigned long physical_chunk_smoke_test(void)
{
unsigned char *small = (unsigned char *) emalloc(37);
unsigned char *large = (unsigned char *) emalloc(96 * 1024);
if (small == 0 || large == 0 || small == large) {
return 0;
}
for (unsigned long index = 0; index < 37; ++index) {
small[index] = (unsigned char) (index + 1);
}
for (unsigned long index = 0; index < 37; ++index) {
if (small[index] != (unsigned char) (index + 1)) {
return 0;
}
}
efree(large);
efree(small);
if (!gc_enabled()) {
return 0;
}
zend_gc_status status;
zend_gc_get_status(&status);
/* A newly enabled collector has an allocated, empty root buffer. */
if (status.runs != 0 || status.collected != 0 || status.num_roots != 0) {
return 0;
}
zend_string *key = zend_string_init("answer", sizeof("answer") - 1, 0);
if (key == 0 || ZSTR_LEN(key) != sizeof("answer") - 1
|| memcmp(ZSTR_VAL(key), "answer", sizeof("answer")) != 0) {
return 0;
}
HashTable table;
zend_hash_init(&table, 4, 0, 0, 0);
zval value;
ZVAL_STRING(&value, "forty-two");
if (zend_hash_add(&table, key, &value) == 0) {
return 0;
}
zval *found = zend_hash_find(&table, key);
if (found == 0 || Z_TYPE_P(found) != IS_STRING
|| !zend_string_equals_literal(Z_STR_P(found), "forty-two")) {
return 0;
}
zend_hash_destroy(&table);
zend_string_release(key);
return zend_memory_usage(1) >= 2ul * 1024ul * 1024ul ? 2 : 0;
}

@ -0,0 +1,57 @@
name: typephp_os
mode: bin
sources:
- src
- freestanding/kernel64-entry.S
- freestanding/kernel.c
- freestanding/memory.c
- freestanding/time.c
- freestanding/zend-allocator.c
- freestanding/bridge.cc
- freestanding/abi/libc.c
- freestanding/abi/cxx.cpp
- freestanding/abi/unimplemented.S
build-dir: build/generated
output: build/kernel64.elf
cxx-std: c++17
include-paths:
- freestanding/abi
defines:
- TYPEPHP_NO_MAIN=1
- PHPX_NO_EXCEPTION=1
- PHPX_NO_RTTI=1
- PHP_NANO_NO_LIBC=1
- __NO_CTYPE=1
- ZEND_MM_ERROR=0
- ZEND_MM_CUSTOM=0
cxx-flags:
- -ffreestanding
- -fno-builtin
- -fno-stack-protector
- -fno-pic
- -fno-pie
- -m64
- -mno-red-zone
- -mcmodel=small
- -fno-rtti
- -fno-threadsafe-statics
c-flags:
- -ffreestanding
- -fno-builtin
- -fno-stack-protector
- -fno-pic
- -fno-pie
- -m64
- -mno-red-zone
- -mcmodel=small
asm-flags:
- -m64
- -mno-red-zone
- -mcmodel=small
ld-flags:
- -nostdlib
- -no-pie
- -Wl,-m,elf_x86_64
- -Wl,--build-id=none
- -Wl,-T,examples/typephp-os/freestanding/kernel64.ld
- -lgcc

@ -0,0 +1,22 @@
<?php
#[NativeFunction]
function kernel_clear(int $color): void {}
#[NativeFunction]
function kernel_put_char(int $ascii, int $color): void {}
#[NativeFunction]
function kernel_write(string $text, int $color): void {}
#[NativeFunction]
function kernel_word_bits(): int { return 0; }
#[NativeFunction]
function kernel_memory_megabytes(): int { return 0; }
#[NativeFunction]
function kernel_chunk_smoke_test(): int { return 0; }
#[NativeFunction]
function kernel_halt(): void {}

@ -0,0 +1,110 @@
<?php
final class KernelGreeting
{
public function render(string $now): string
{
return $now . ' Hello TypePHP-OS!';
}
}
function writeLine(string $text, int $color): void
{
kernel_write($text, $color);
kernel_put_char(10, $color);
}
function writeNumber(int $number, int $color): void
{
if ($number === 0) {
kernel_put_char(48, $color);
return;
}
$digits = std::vector(Type::Int);
while ($number > 0) {
$digit = $number % 10;
$digits[] = $digit;
$number = (int) (($number - $digit) / 10);
}
for ($index = count($digits) - 1; $index >= 0; $index--) {
kernel_put_char(48 + $digits[$index], $color);
}
}
function isPrime(int $number): bool
{
if ($number < 2) {
return false;
}
for ($divisor = 2; $divisor * $divisor <= $number; $divisor++) {
if ($number % $divisor === 0) {
return false;
}
}
return true;
}
function runFeatureSelfCheck(): void
{
kernel_clear(0);
writeLine('TypePHP OS POC', 10);
kernel_write('Int bits: ', 15);
writeNumber(kernel_word_bits(), 15);
kernel_put_char(10, 15);
kernel_write('RAM MiB: ', 15);
writeNumber(kernel_memory_megabytes(), 15);
kernel_put_char(10, 15);
kernel_write('Zend MiB: ', 15);
writeNumber(kernel_chunk_smoke_test(), 15);
kernel_put_char(10, 15);
$checks = ['Zend string/array: OK', 'Kernel is!'];
foreach ($checks as $check) {
writeLine($check, 10);
}
}
function runPrimeDemo(int $limit): void
{
$primes = std::vector(Type::Int);
for ($candidate = 0; $candidate <= $limit; $candidate++) {
if (isPrime($candidate)) {
$primes[] = $candidate;
}
}
kernel_write('Calculate primes: 0-', 11);
writeNumber($limit, 11);
kernel_put_char(10, 11);
kernel_write('Prime count: ', 15);
writeNumber(count($primes), 15);
kernel_put_char(10, 15);
kernel_write('Prime list: ', 15);
for ($index = 0; $index < count($primes); $index++) {
if ($index !== 0) {
kernel_write(', ', 15);
}
writeNumber($primes[$index], 15);
}
kernel_put_char(10, 15);
}
function main(): void
{
runFeatureSelfCheck();
runPrimeDemo(100);
$greeting = new KernelGreeting();
while (true) {
sleep(2);
kernel_write($greeting->render(date('Y-m-d H:i:s')), 15);
kernel_put_char(10, 15);
}
}

@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
kernel=${1:?kernel ELF is required}
log=${2:?log path is required}
if timeout 8 qemu-system-x86_64 \
-kernel "${kernel}" \
-display none \
-serial stdio \
-monitor none \
-no-reboot \
-no-shutdown \
-device isa-debug-exit,iobase=0xf4,iosize=0x04 \
>"${log}" 2>&1; then
status=0
else
status=$?
fi
if [[ ${status} -ne 124 ]]; then
cat "${log}"
echo "QEMU exited with ${status}; expected the resident kernel loop to reach the timeout" >&2
exit 1
fi
grep -q "TypePHP OS POC" "${log}"
grep -q "Int bits: 64" "${log}"
grep -Eq '^RAM MiB: [1-9][0-9]*' "${log}"
grep -q "Zend MiB: 2" "${log}"
grep -q "Zend string/array: OK" "${log}"
grep -q "Kernel is!" "${log}"
grep -q "Calculate primes: 0-100" "${log}"
grep -q "Prime count: 25" "${log}"
grep -q "Prime list: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97" "${log}"
grep -Eq '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} Hello TypePHP-OS![[:space:]]*$' "${log}"
cat "${log}"

@ -102,6 +102,7 @@ class BackendTest extends TestCase
'user_defines' => ['FEATURE_X=1'],
'lto' => true,
'is_zts' => true,
'cflags' => '/experimental:c11atomics',
]);
$this->assertStringContainsString('/TC', $cmd);
@ -111,6 +112,7 @@ class BackendTest extends TestCase
$this->assertStringContainsString('/DFEATURE_X=1', $cmd);
$this->assertStringContainsString('/GL', $cmd);
$this->assertStringContainsString('/DZTS', $cmd);
$this->assertStringContainsString('/experimental:c11atomics', $cmd);
$this->assertStringNotContainsString('/EHsc', $cmd);
$this->assertStringNotContainsString('/std:', $cmd);
}
@ -214,6 +216,7 @@ class BackendTest extends TestCase
'march' => 'native',
'target_platform' => 'aarch64-linux-gnu',
'build_mode' => 'ext',
'cflags' => '-ffreestanding -fno-builtin',
]);
$this->assertStringContainsString('-fsanitize=address', $cmd);
@ -224,6 +227,19 @@ class BackendTest extends TestCase
$this->assertStringContainsString('-march=native', $cmd);
$this->assertStringContainsString('--target=aarch64-linux-gnu', $cmd);
$this->assertStringContainsString('-fPIC', $cmd);
$this->assertStringContainsString('-ffreestanding -fno-builtin', $cmd);
}
public function testGccBuildAssemblerCommandUsesNativeFlags(): void
{
$compiler = new Gcc(new Linux());
$cmd = $compiler->buildNativeCompileCommand('entry.S', 'entry.o', [
'nativeflags' => '-m64 -mno-red-zone',
], 'assembler');
$this->assertStringContainsString('-x assembler', $cmd);
$this->assertStringContainsString('-m64 -mno-red-zone', $cmd);
}
public function testGccBuildLinkCommandIncludesPlatformPathsOptionsAndLibraries(): void

@ -776,6 +776,37 @@ YAML, 'myproject.yml', 'nested/config');
$this->assertSame([$projectDir . '/libs'], $this->compiler->getLinkPaths());
$this->assertSame($projectDir . '/bin', $this->getPropertyValue('outputDir'));
$this->assertSame('my_app', $this->getPropertyValue('targetName'));
$this->assertSame($projectDir . '/bin/my-app', $this->invokeMethod('getTargetFileName'));
}
public function testParseProjectYamlAddsPrecompiledObjectsAsGenericLinkInputs(): void
{
$projectFile = $this->createProjectFile(<<<'YAML'
sources:
- main.php
objects:
- build/startup.o
- path: build/platform.obj
if: PHP_OS_FAMILY == "Linux"
- path: build/windows.obj
if: PHP_OS_FAMILY == "Windows"
cxx-flags: [-fno-exceptions, -fno-rtti]
c-flags: -ffreestanding -fno-builtin
asm-flags:
- -m64
- -mno-red-zone
YAML, 'objects.yml', 'native-objects');
$this->invokeMethod('parseProjectYaml', $projectFile);
$projectDir = dirname($projectFile);
$this->assertSame([
$projectDir . '/build/startup.o',
$projectDir . '/build/platform.obj',
], $this->compiler->getProjectObjectFiles());
$this->assertSame('-fno-exceptions -fno-rtti', $this->getPropertyValue('cxxFlags'));
$this->assertSame('-ffreestanding -fno-builtin', $this->getPropertyValue('cFlags'));
$this->assertSame('-m64 -mno-red-zone', $this->getPropertyValue('asmFlags'));
}
public function testCliOutputOverridesYamlOutputOnlyWhenCommandLineArgumentsAreApplied(): void

@ -30,7 +30,11 @@ class GeneratedCodeIndentationTest extends \PHPUnit\Framework\TestCase
$code,
);
$this->assertStringContainsString(
"\ttry {\n\t\tphp::checkCallArgCount(1, 1, false);",
"\tPHPX_TRY {\n\t\tphp::checkCallArgCount(1, 1, false);",
$code,
);
$this->assertStringContainsString(
"\t} PHPX_CATCH(zend_object *, typephp_wrapper_exception) {",
$code,
);
$this->assertStringNotContainsString("\ntry {", $code);

@ -106,6 +106,10 @@ abstract class GccLikeBackend extends CompilerBackend
. ' -mllvm -wasm-use-legacy-eh=false';
}
if (!$includeCppStd && !empty($config['cflags'])) {
$cmd .= ' ' . $config['cflags'];
}
if (!empty($config['target_platform'])) {
$cmd .= ' --target=' . $config['target_platform'];
}
@ -247,6 +251,9 @@ abstract class GccLikeBackend extends CompilerBackend
}
$cmd .= $this->buildCompileOptions($options);
if (!empty($options['nativeflags'])) {
$cmd .= ' ' . $options['nativeflags'];
}
return $cmd;
}

@ -106,6 +106,8 @@ class Msvc extends CompilerBackend
if (!empty($config['forced_include'])) {
$cmd .= ' /FI' . escapeshellarg($config['forced_include']);
}
} elseif (!empty($config['cflags'])) {
$cmd .= ' ' . $config['cflags'];
}
$cmd .= ' /nologo';

@ -104,7 +104,9 @@ trait NativeCommandOptionsTrait
protected function getCCompileCommandOptions(): CompileOptions
{
$options = $this->getCommonCompileCommandOptions();
$options = $options->with('suppressed_warnings', ['4244', '4146']);
$options = $options
->with('cflags', $this->cFlags)
->with('suppressed_warnings', ['4244', '4146']);
return $this->isNanoMode() ? $options->with('c_std', 'c11') : $options;
}
@ -122,6 +124,10 @@ trait NativeCommandOptionsTrait
if ($language === 'objective-c++') {
$options = $options->with('cpp_std', $this->cxxStd)->with('cxxflags', $this->cxxFlags);
} elseif ($language === 'assembler') {
$options = $options->with('nativeflags', $this->asmFlags);
} elseif ($language === 'objective-c') {
$options = $options->with('nativeflags', $this->cFlags);
}
return $options;

@ -128,6 +128,7 @@ trait SourcePipelineTrait
// Apply command-line arguments after all configuration is loaded (so they
// take the highest precedence)
$this->applyCommandLineArguments();
$this->validateProjectObjectFiles();
// The generated public import stub is an output artifact, not an input
// of the library that produced it. Exclude a previous build's copy when

@ -500,12 +500,16 @@ class CompilerBase implements PropertyAccessContext
protected int $maxJob = 4;
protected string $buildMode = self::BUILD_MODE_BIN;
protected string $cxxFlags = '';
protected string $cFlags = '';
protected string $asmFlags = '';
protected string $cxxStd = 'c++17';
protected string $march = ''; // --march: target CPU instruction set (e.g. native, x86-64-v3)
protected string $targetPlatform = ''; // --target-platform: cross-compilation target triple (e.g. aarch64-linux-gnu)
protected string $ldflags = '';
protected array $linkLibs = []; // --link-lib / -l: user-specified libraries to link
protected array $linkPaths = []; // --link-path / -L: user-specified library search paths
/** @var list<string> Precompiled project object files linked into the target. */
protected array $projectObjectFiles = [];
/** @var list<string> Required PHP modules recorded in zend_module_entry.deps. */
protected array $extensionDependencies = [];
protected bool $debug = false;

@ -31,20 +31,32 @@ final class ProjectYamlLoader
/** @return array{0: string, 1: string|null} */
public function parseSourceEntry(mixed $entry): array
{
return $this->parsePathEntry($entry, 'sources');
}
/** @return array{0: string, 1: string|null} */
public function parseObjectEntry(mixed $entry): array
{
return $this->parsePathEntry($entry, 'objects');
}
/** @return array{0: string, 1: string|null} */
private function parsePathEntry(mixed $entry, string $key): array
{
if (is_string($entry)) {
return [$entry, null];
}
if (!is_array($entry)) {
($this->error)('Each `sources` entry must be a string or map');
($this->error)("Each `{$key}` entry must be a string or map");
}
$path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null;
if (!is_string($path) || trim($path) === '') {
($this->error)('Conditional `sources` entries must include a non-empty `path`');
($this->error)("Conditional `{$key}` entries must include a non-empty `path`");
}
$condition = $entry['if'] ?? $entry['when'] ?? null;
if ($condition !== null && !is_string($condition)) {
($this->error)('Source condition must be a string');
($this->error)(ucfirst(rtrim($key, 's')) . ' condition must be a string');
}
return [$path, $condition];
}

@ -84,6 +84,8 @@ class Translator extends Preprocessor
public const string APP_NAME = 'TypePHP Compiler (AOT)';
protected bool $hasExplicitOutput = false;
/** Exact output stem; generated C/C++ identifiers still use targetName. */
protected ?string $explicitOutputBasename = null;
protected ?string $explicitOutputExtension = null;
protected array $sourceDirs = [];
private ?ProjectYamlLoader $projectYamlLoader = null;
@ -870,12 +872,13 @@ class Translator extends Preprocessor
}
$this->hasExplicitOutput = true;
$this->explicitOutputBasename = $path;
$this->setTargetName($path);
}
protected function getTargetFileName(): string
{
$targetFile = $this->targetName;
$targetFile = $this->explicitOutputBasename ?? $this->targetName;
if ($this->isBuildModeLib() && !$this->isWindows() && !$this->hasExplicitOutput) {
$targetFile = 'lib' . $targetFile;
}
@ -3056,6 +3059,24 @@ CODE;
$cfg = $this->getProjectYamlLoader()->load($path);
$projectDir = dirname($path);
$objects = $cfg['objects'] ?? [];
if (!is_array($objects)) {
$this->error('`objects` must be an array');
}
foreach ($objects as $entry) {
[$object, $condition] = $this->getProjectYamlLoader()->parseObjectEntry($entry);
if ($condition !== null && !$this->evaluateProjectYamlCondition($condition)) {
continue;
}
$object = $this->resolvePath($object, $projectDir, 'Object path');
if (!in_array(strtolower(pathinfo($object, PATHINFO_EXTENSION)), ['o', 'obj'], true)) {
$this->error("Project object must use the .o or .obj extension: {$object}");
}
$object = realpath($object) ?: $object;
$this->projectObjectFiles[] = $object;
}
$this->projectObjectFiles = array_values(array_unique($this->projectObjectFiles));
if (array_key_exists('php-version', $cfg) && !$this->climate->arguments->defined('php-version')) {
$this->setPhpVersion((string) $cfg['php-version']);
}
@ -3135,6 +3156,21 @@ CODE;
}
}
// Read C and assembler flags independently. This keeps project-level
// native builds generic while allowing each language its own ABI flags.
$cFlags = $cfg['c-flags'] ?? null;
if (!empty($cFlags)) {
$this->cFlags = is_array($cFlags)
? implode(' ', $cFlags)
: str_replace("\n", ' ', (string) $cFlags);
}
$asmFlags = $cfg['asm-flags'] ?? null;
if (!empty($asmFlags)) {
$this->asmFlags = is_array($asmFlags)
? implode(' ', $asmFlags)
: str_replace("\n", ' ', (string) $asmFlags);
}
// Read cxx-std
$cxxStd = $cfg['cxx-std'] ?? null;
if (!empty($cxxStd)) {
@ -3337,6 +3373,27 @@ CODE;
return $this->filterIgnoredFiles($list);
}
/** @return list<string> */
public function getProjectObjectFiles(): array
{
return $this->projectObjectFiles;
}
protected function validateProjectObjectFiles(): void
{
if ($this->isDryRun()) {
return;
}
foreach ($this->projectObjectFiles as $object) {
if (!is_file($object)) {
$this->error(
"Precompiled project object not found: {$object}\n"
. 'Compile project C/C++/assembly sources before invoking tpc.'
);
}
}
}
/**
* @return array{0: string, 1: string|null}
*/
@ -5055,7 +5112,7 @@ CODE;
// returns with EG(exception) set. Convert back to normal Zend exception
// propagation at the outermost wrapper.
$this->indentLevel++;
$cppCode = $this->getIndent() . 'try {' . PHP_EOL;
$cppCode = $this->getIndent() . 'PHPX_TRY {' . PHP_EOL;
$this->indentLevel++;
$argCountCheck = $this->genParameterCountCheck(
@ -5177,7 +5234,8 @@ CODE;
}
}
$this->indentLevel--;
$cppCode .= $this->getIndent() . '} catch (zend_object *) {' . PHP_EOL;
$cppCode .= $this->getIndent()
. '} PHPX_CATCH(zend_object *, typephp_wrapper_exception) {' . PHP_EOL;
$this->indentLevel++;
$cppCode .= $this->getIndent() . '/* EG(exception) is already set; return control to ZendVM for frame cleanup. */' . PHP_EOL;
$this->indentLevel--;

@ -111,7 +111,10 @@ function main(int $argc, array $argv): void
}
// Compile all C++ source files.
$objectFiles = $translator->compile($sourceFiles);
$objectFiles = [
...$translator->compile($sourceFiles),
...$translator->getProjectObjectFiles(),
];
// Link all object files to produce the executable.
$binaryFile = $translator->build($objectFiles);
// If --run / -r was specified, execute immediately after compilation.

Loading…
Cancel
Save