Compare commits
172 Commits
speed_buil
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
7973f99f47 | 18 hours ago |
|
|
74274f8328 | 1 day ago |
|
|
6693d95cd6 | 1 day ago |
|
|
8f32497de7 | 1 day ago |
|
|
1b235d0769 | 1 day ago |
|
|
8c6ea1d14e | 1 day ago |
|
|
0ac6dfe499 | 1 day ago |
|
|
a6cc11c6d7 | 1 day ago |
|
|
2d14c9e432 | 1 day ago |
|
|
7f587749f1 | 1 day ago |
|
|
821f639e8f | 1 day ago |
|
|
983333dc2e | 1 day ago |
|
|
978e553480 | 1 day ago |
|
|
e4817819dc | 1 day ago |
|
|
8a30c5e58d | 1 day ago |
|
|
0061d1595a | 1 day ago |
|
|
2c74be470e | 1 day ago |
|
|
2986f3db70 | 1 day ago |
|
|
e4bb7b2605 | 2 days ago |
|
|
305d0d09d9 | 2 days ago |
|
|
a5700d24c9 | 2 days ago |
|
|
642e9a7e4b | 2 days ago |
|
|
422bdc4ce6 | 2 days ago |
|
|
208541d00b | 2 days ago |
|
|
19a570ff3f | 2 days ago |
|
|
178636a577 | 2 days ago |
|
|
1118c1e8ec | 2 days ago |
|
|
02fc0fb319 | 2 days ago |
|
|
3def25cc3c | 2 days ago |
|
|
44095252d2 | 2 days ago |
|
|
9f1cf07511 | 2 days ago |
|
|
36fd0228f0 | 2 days ago |
|
|
4c59d9a21b | 2 days ago |
|
|
b2246cd456 | 2 days ago |
|
|
795a8e0a1d | 2 days ago |
|
|
9fc5755544 | 2 days ago |
|
|
6dcafb652b | 2 days ago |
|
|
23a2bf44d7 | 2 days ago |
|
|
6d45c64b4d | 3 days ago |
|
|
c5f478766e | 3 days ago |
|
|
f64a1d5cb7 | 3 days ago |
|
|
00420215b3 | 3 days ago |
|
|
2471ed1db7 | 3 days ago |
|
|
0a97daa663 | 4 days ago |
|
|
4e00799c4a | 5 days ago |
|
|
41e11a66e6 | 5 days ago |
|
|
0f6cc17450 | 5 days ago |
|
|
101b9f6261 | 5 days ago |
|
|
65a44c7f65 | 5 days ago |
|
|
e722501b3d | 5 days ago |
|
|
d80f4a0d20 | 5 days ago |
|
|
4713a58b06 | 5 days ago |
|
|
cb1e12f11f | 5 days ago |
|
|
7418b6247e | 6 days ago |
|
|
65d3710a61 | 6 days ago |
|
|
70d13a3601 | 6 days ago |
|
|
4b8d0eb685 | 6 days ago |
|
|
a08f73c995 | 7 days ago |
|
|
729000fa0c | 7 days ago |
|
|
0d0e62fbc7 | 7 days ago |
|
|
ab1854beb0 | 7 days ago |
|
|
d33073c021 | 7 days ago |
|
|
68bdedaa31 | 1 week ago |
|
|
d1704b42ef | 1 week ago |
|
|
f1ef66622c | 1 week ago |
|
|
244fa4d8b2 | 1 week ago |
|
|
7fece68f12 | 1 week ago |
|
|
464de23051 | 1 week ago |
|
|
2ffd4ded19 | 1 week ago |
|
|
2e6ddf98f1 | 1 week ago |
|
|
c9666664aa | 1 week ago |
|
|
587818d5f9 | 1 week ago |
|
|
7d1a90a340 | 1 week ago |
|
|
13344ace72 | 1 week ago |
|
|
7cf265cbc7 | 1 week ago |
|
|
de8fdaf464 | 1 week ago |
|
|
d5f0a113e6 | 1 week ago |
|
|
981b8f54d7 | 2 weeks ago |
|
|
af0b6f9582 | 2 weeks ago |
|
|
2f229ee0ac | 2 weeks ago |
|
|
ea08344c92 | 2 weeks ago |
|
|
1456305ade | 2 weeks ago |
|
|
5162261c75 | 2 weeks ago |
|
|
a536172f82 | 2 weeks ago |
|
|
aaf1ec11b3 | 2 weeks ago |
|
|
e4f6475452 | 2 weeks ago |
|
|
96960e9da1 | 2 weeks ago |
|
|
bca77588b5 | 2 weeks ago |
|
|
f79d40f8ee | 2 weeks ago |
|
|
496e6a4d69 | 2 weeks ago |
|
|
a510530d9b | 2 weeks ago |
|
|
6c9adbccad | 2 weeks ago |
|
|
343084c4ba | 2 weeks ago |
|
|
af7ae9df82 | 2 weeks ago |
|
|
42e96f1103 | 2 weeks ago |
|
|
23e88efb6a | 2 weeks ago |
|
|
463b427dc6 | 2 weeks ago |
|
|
e1018ca87e | 2 weeks ago |
|
|
bd6335ec3b | 2 weeks ago |
|
|
a79610fb2c | 2 weeks ago |
|
|
5fb3fdf490 | 2 weeks ago |
|
|
2a1ca3b1a0 | 2 weeks ago |
|
|
01ea7c40be | 2 weeks ago |
|
|
8aa8cdb065 | 2 weeks ago |
|
|
6e5fadcbc6 | 2 weeks ago |
|
|
18dd9a8a32 | 2 weeks ago |
|
|
e07b97d6cf | 2 weeks ago |
|
|
8a3b5ae9a4 | 2 weeks ago |
|
|
8e48b9d7fe | 2 weeks ago |
|
|
f5ca997e65 | 2 weeks ago |
|
|
c47ef90593 | 2 weeks ago |
|
|
b77fee697e | 2 weeks ago |
|
|
7190bea4a5 | 2 weeks ago |
|
|
6fb8187b07 | 2 weeks ago |
|
|
997bbffb65 | 2 weeks ago |
|
|
8118fbd3dc | 2 weeks ago |
|
|
e5f61eabfe | 2 weeks ago |
|
|
a3ea91918d | 2 weeks ago |
|
|
3b51eaa6d0 | 2 weeks ago |
|
|
0ba5079336 | 2 weeks ago |
|
|
7d7fa75d6f | 2 weeks ago |
|
|
64afbc9656 | 2 weeks ago |
|
|
6c97b4f301 | 2 weeks ago |
|
|
f61a542255 | 2 weeks ago |
|
|
3b6a48ef74 | 2 weeks ago |
|
|
22160661a9 | 2 weeks ago |
|
|
069d3e61bb | 2 weeks ago |
|
|
2be13eefcd | 3 weeks ago |
|
|
cb03570212 | 3 weeks ago |
|
|
70927ee68a | 3 weeks ago |
|
|
a4f1188a7e | 3 weeks ago |
|
|
1072e9d1c7 | 3 weeks ago |
|
|
1847b4c926 | 3 weeks ago |
|
|
f630fcf661 | 3 weeks ago |
|
|
525a4e4412 | 3 weeks ago |
|
|
b1b40d6a09 | 3 weeks ago |
|
|
8d4801ebd2 | 3 weeks ago |
|
|
ca41af9bb5 | 3 weeks ago |
|
|
833b74b344 | 3 weeks ago |
|
|
b69582011a | 3 weeks ago |
|
|
c4b8db030d | 3 weeks ago |
|
|
7f46445a38 | 3 weeks ago |
|
|
39354af70b | 3 weeks ago |
|
|
dbd12d249d | 3 weeks ago |
|
|
1baefe166f | 3 weeks ago |
|
|
c4404c15c5 | 3 weeks ago |
|
|
abbaa11c7c | 3 weeks ago |
|
|
3d36928e8b | 3 weeks ago |
|
|
ac16f5fbac | 3 weeks ago |
|
|
9e26887c03 | 3 weeks ago |
|
|
07c7d4b84a | 3 weeks ago |
|
|
9992793a02 | 3 weeks ago |
|
|
8503c7c32c | 3 weeks ago |
|
|
cee9ee6e1b | 3 weeks ago |
|
|
c3614aa6d3 | 3 weeks ago |
|
|
580d877ecd | 3 weeks ago |
|
|
3b3711c139 | 3 weeks ago |
|
|
b71ef5ca1a | 3 weeks ago |
|
|
9d1ba7ee7a | 3 weeks ago |
|
|
8dd33c1400 | 3 weeks ago |
|
|
db4b7f9e28 | 3 weeks ago |
|
|
2310c574ab | 3 weeks ago |
|
|
8a23368cf1 | 3 weeks ago |
|
|
ec298e66fe | 3 weeks ago |
|
|
6d7e68a9ff | 3 weeks ago |
|
|
a7b16dc1be | 3 weeks ago |
|
|
c6c04d7e96 | 3 weeks ago |
|
|
78135d75ef | 3 weeks ago |
|
|
f2c9309857 | 3 weeks ago |
|
|
09b99e357d | 3 weeks ago |
|
|
560ea46692 | 3 weeks ago |
|
|
a92fc0dfd7 | 3 weeks ago |
851 changed files with 62654 additions and 6650 deletions
@ -1,274 +0,0 @@ |
|||||||
# Code Reuse Improvement Plan |
|
||||||
|
|
||||||
## Analysis Summary |
|
||||||
|
|
||||||
| Metric | Value | |
|
||||||
|--------|-------| |
|
||||||
| Total source lines | ~11,000 (PHP only) | |
|
||||||
| CompilerBase | 5,917 lines, 269 methods, 20 traits | |
|
||||||
| Gcc↔Clang duplication | ~70-80% of methods | |
|
||||||
| Linux↔Macos duplication | ~80% of methods | |
|
||||||
| `fatalError()` call sites | 174+ across codebase | |
|
||||||
| Test setUp/tearDown dup | 4+ test classes | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Phase 1: High-Impact Backend/Platform Deduplication (P0) |
|
||||||
|
|
||||||
### 1.1 Extract `UnixPlatform` base class |
|
||||||
|
|
||||||
**Files**: `Platform/Linux.php` (249 lines), `Platform/Macos.php` (278 lines) |
|
||||||
|
|
||||||
These 13 methods are 100% identical between Linux and Macos: |
|
||||||
- `getIncludeFlags()`, `getLibraryPathFlags()`, `getObjectExtension()`, `getExecutableExtension()`, `getPathSeparator()`, `getPhpDir()`, `getRpathOptions()`, `getPicFlag()`, `buildPhpIncludePaths()`, `findPhpConfig()`, `buildPhpLibPaths()` |
|
||||||
|
|
||||||
Near-identical with minor parameterization: |
|
||||||
- `getLibraryFlags()` — only the regex differs (`.a|.so` vs `.a|.dylib`) |
|
||||||
- `detectPhpLibs()` — only the lib name differs (`libphp.so` vs `libphp.dylib`) |
|
||||||
|
|
||||||
**Plan**: Create `UnixPlatform extends PlatformBase` between `PlatformBase` and `Linux`/`Macos`. Move all identical methods up. Add abstract `getSharedLibraryExtension()` (already exists) and a protected `getSharedLibName()` for the single differing method. |
|
||||||
|
|
||||||
**Expected savings**: ~180 lines removed, ~150 lines added = net ~30 lines but massive maintainability gain. |
|
||||||
|
|
||||||
### 1.2 Extract `GccLikeBackend` base class |
|
||||||
|
|
||||||
**Files**: `Backend/Gcc.php` (379 lines), `Backend/Clang.php` (515 lines) |
|
||||||
|
|
||||||
These methods are structurally identical with only Windows-specific branching: |
|
||||||
- `compileFile()`, `linkObjects()`, `buildCompileCommand()`, `buildCCompileCommand()`, `buildNativeCompileCommand()`, `buildLinkCommand()`, `buildCompileOptions()`, `buildLinkOptions()`, `buildFullCompileOptions()`, `buildFullLinkOptions()` |
|
||||||
|
|
||||||
**Plan**: Create `GccLikeBackend extends CompilerBackend` with all shared logic. Define template-method hooks for the differences: |
|
||||||
- `getCompilerSpecificFlags()` — empty for Gcc, MSVC compat flags for Clang/Windows |
|
||||||
- `getOutputFlag($isWindows)` — `-o` vs `/OUT:` |
|
||||||
- `getSanitizerFlag($type)` — handle the `address`/`addr` aliasing difference |
|
||||||
- `getPICHandling($config)` — Gcc always adds `-fPIC`, Clang skips on Windows |
|
||||||
|
|
||||||
**Expected savings**: ~250+ lines removed from Gcc.php and Clang.php. Msvc.php is sufficiently different (different flag syntax) to remain standalone. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Phase 2: CompilerBase Internal Deduplication (P1) |
|
||||||
|
|
||||||
### 2.1 Consolidate Big* type dispatch in BinaryOpTrait |
|
||||||
|
|
||||||
**File**: `Parser/BinaryOpTrait.php` (lines 31-107) |
|
||||||
|
|
||||||
The three blocks for BigFloat (lines 31-54), Decimal (lines 56-76), and BigInt (lines 78-107) in `parseBinaryOp()` share identical structure: |
|
||||||
1. Check if either operand is the big type |
|
||||||
2. Guard against incompatible mixing |
|
||||||
3. Convert the non-matching operand |
|
||||||
4. Dispatch to arithmetic or comparison operator |
|
||||||
|
|
||||||
**Plan**: Extract `parseBigNumBinaryOp(string $type, string $left, string $right, ...)` parameterized by type name, conversion function, and operator maps. Same refactoring applies to `genBigNumericCmp()` (lines 315-355). |
|
||||||
|
|
||||||
**Expected savings**: ~40 lines. |
|
||||||
|
|
||||||
### 2.2 Data-driven operator dispatch tables |
|
||||||
|
|
||||||
**Files**: `Parser/BinaryOpTrait.php` (16 wrapper methods), `Parser/AssignOpTrait.php` (14 wrapper methods) |
|
||||||
|
|
||||||
30+ thin methods that are just `parseBinaryOp($left, $right, '+')` / `parseAssignOp($node, '+=')`. |
|
||||||
|
|
||||||
**Plan**: Replace with a static map in `parseExpr()`: |
|
||||||
```php |
|
||||||
private const BINARY_OP_MAP = [ |
|
||||||
'Expr_BinaryOp_Plus' => '+', |
|
||||||
'Expr_BinaryOp_Minus' => '-', |
|
||||||
// ... |
|
||||||
]; |
|
||||||
private const ASSIGN_OP_MAP = [ |
|
||||||
'Expr_AssignOp_Plus' => '+=', |
|
||||||
// ... |
|
||||||
]; |
|
||||||
``` |
|
||||||
|
|
||||||
**Expected savings**: ~200 lines removed (boilerplate method bodies). |
|
||||||
|
|
||||||
### 2.3 Deduplicate call dispatch patterns |
|
||||||
|
|
||||||
**Files**: `CompilerBase.php` (`parseFuncCall`, `parseMethodCall`, `parseStaticCall` — ~300 lines combined), `UniversalMethodCall.php` (`tryOptimizePhpFn` vs `dispatchFuncCall`) |
|
||||||
|
|
||||||
These share the same overall flow: resolve callable → try native/optimized path → on `PlaceHolder` fall back to placeholder → parse args → wrap in `php::call()`. Additionally, `tryOptimizePhpFn()` (UniversalMethodCall lines 720-772) duplicates the argument type conversion logic already present in `dispatchFuncCall()` (FuncCallOptimizer lines 234-269). |
|
||||||
|
|
||||||
**Plan**: Extract a shared `resolveCall(CallLike $expr, ...)` method. Unify arg conversion so `tryOptimizePhpFn` delegates to `dispatchFuncCall` instead of reimplementing it. |
|
||||||
|
|
||||||
**Expected savings**: ~40 lines, fixes double-calculation of arg conversions. |
|
||||||
|
|
||||||
### 2.4 Deduplicate UNIVERSAL_METHODS math entries |
|
||||||
|
|
||||||
**File**: `UniversalMethodCall.php` (lines 12-69) |
|
||||||
|
|
||||||
The INT block (lines 12-41) and FLOAT block (lines 42-69) contain 20 identical math method entries (`abs`, `ceil`, `floor`, `sqrt`, `sin`, `cos`, etc.) differing only in `return_type`. Also the `calc_op` entries (add/sub/mul/div) are duplicated. |
|
||||||
|
|
||||||
**Plan**: Define math method names once in a shared array, generate both INT and FLOAT entries in the constructor with the appropriate `return_type`. |
|
||||||
|
|
||||||
**Expected savings**: ~25 lines of config data. |
|
||||||
|
|
||||||
### 2.5 Deduplicate constant folding methods |
|
||||||
|
|
||||||
**File**: `Optimizer/FuncCallOptimizer.php` (lines 515-593) |
|
||||||
|
|
||||||
8 methods (`doFoldStringLen`, `doFoldStringCase`, `doFoldCmp2`, `doFoldCmp3`, `doFoldCountLiteral`, `doFoldKnownClass`, `doFoldKnownConstant`, `doFoldSsaType`) follow the identical pattern: extract args → check types → compute → return literal or false. |
|
||||||
|
|
||||||
**Plan**: Create a generic `tryFold(callable $check, callable $compute)` that handles the arg extraction and short-circuit boilerplate. Each folder becomes a one-liner. |
|
||||||
|
|
||||||
**Expected savings**: ~50 lines. |
|
||||||
|
|
||||||
### 2.6 Remove MSVC compat flag duplication in Clang |
|
||||||
|
|
||||||
**File**: `Backend/Clang.php` |
|
||||||
|
|
||||||
The 4-line MSVC compatibility block (`-fms-compatibility`, `-fms-compatibility-version=19.40`, `-fdelayed-template-parsing`, `-fms-extensions`) appears 7 times (compileFile, buildCompileCommand, buildCCompileCommand, buildNativeCompileCommand, buildFullCompileOptions, buildCompileOptions, buildLinkOptions). |
|
||||||
|
|
||||||
**Plan**: Extract `private function getMsvcCompatFlags(): string` method. Called once per method that needs it instead of repeated inline. |
|
||||||
|
|
||||||
**Expected savings**: ~24 lines, single point of change if MSVC compat flags need updating. |
|
||||||
|
|
||||||
### 2.7 Merge return-check blocks |
|
||||||
|
|
||||||
**File**: `CompilerBase.php`, `parseReturn()` (line 1621) and `genReturnCode()` (line 5741) |
|
||||||
|
|
||||||
Identical 7-line union type check blocks. |
|
||||||
|
|
||||||
**Plan**: Extract `genUnionReturnWrapper(string $exprVar)` method. |
|
||||||
|
|
||||||
**Expected savings**: ~10 lines, eliminates drift risk. |
|
||||||
|
|
||||||
### 2.8 Fix `buildCCompileCommand()` inconsistency between Gcc and Clang |
|
||||||
|
|
||||||
**Files**: `Backend/Gcc.php` (lines 131-137), `Backend/Clang.php` (lines 198-206) |
|
||||||
|
|
||||||
Gcc unconditionally appends `-O$level` then conditionally appends `-g`. Clang treats debug and optimization as mutually exclusive (`if debug: -O0 -g` else `-O$level`). This is a behavioral inconsistency between backends implementing the same abstract method. |
|
||||||
|
|
||||||
**Plan**: Standardize on one behavior (the Clang pattern of `-O0 -g` for debug is the correct one — debug builds should not optimize). This will be automatically resolved by Phase 1.2 (GccLikeBackend). |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Phase 3: Structural Improvements (P2) |
|
||||||
|
|
||||||
### 3.1 Entity flag-check consistency |
|
||||||
|
|
||||||
**File**: `Entity/PropertyDef.php` has `isPrivate()`, `isProtected()`, `isPublic()`, `isStatic()`. `Entity/MethodDef.php` has none — flag checks are done inline in CompilerBase. |
|
||||||
|
|
||||||
**Plan**: Add a `HasFlags` trait used by both `PropertyDef` and `MethodDef`: |
|
||||||
```php |
|
||||||
trait HasFlags { |
|
||||||
public function isPrivate(): bool { return $this->flags & Modifiers::PRIVATE; } |
|
||||||
public function isProtected(): bool { return $this->flags & Modifiers::PROTECTED; } |
|
||||||
public function isPublic(): bool { return !$this->isPrivate() && !$this->isProtected(); } |
|
||||||
public function isStatic(): bool { return $this->flags & Modifiers::STATIC; } |
|
||||||
public function isAbstract(): bool { return $this->flags & Modifiers::ABSTRACT; } |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
**Expected savings**: Removes inline flag checks from CompilerBase, adds clarity. |
|
||||||
|
|
||||||
### 3.2 Test infrastructure base class |
|
||||||
|
|
||||||
**Files**: `phpunit/src/AstNodeTypeTest.php`, `CompilerBaseAdapterTest.php`, `TraitsTest.php`, `PreprocessorTest.php` |
|
||||||
|
|
||||||
All 4 duplicate the same setUp/tearDown pattern: create temp dir, `CompilerTest::create()`, recursive cleanup. |
|
||||||
|
|
||||||
**Plan**: Add `CompilerTestCase extends \PHPUnit\Framework\TestCase` to `phpunit/bootstrap.php`: |
|
||||||
```php |
|
||||||
abstract class CompilerTestCase extends TestCase { |
|
||||||
protected string $tmpDir; |
|
||||||
protected CompilerTest $compiler; |
|
||||||
|
|
||||||
protected function setUp(): void { |
|
||||||
parent::setUp(); |
|
||||||
$this->tmpDir = sys_get_temp_dir() . '/compiler_test_' . uniqid(); |
|
||||||
mkdir($this->tmpDir, 0777, true); |
|
||||||
$this->compiler = CompilerTest::create($this->tmpDir); |
|
||||||
} |
|
||||||
|
|
||||||
protected function tearDown(): void { |
|
||||||
parent::tearDown(); |
|
||||||
// recursive cleanup |
|
||||||
} |
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
### 3.3 Eliminate `buildFull*Options` / `build*Options` duality |
|
||||||
|
|
||||||
**Files**: `Backend/Gcc.php`, `Backend/Clang.php`, `Backend/Msvc.php` |
|
||||||
|
|
||||||
All three backends implement both `buildFullCompileOptions()` / `buildCompileOptions()` and `buildFullLinkOptions()` / `buildLinkOptions()`. The "full" variants are subsets of the "standard" variants working from differently-keyed option arrays. They have drifted independently (e.g., RPATH handling differs between the two in Gcc/Clang). |
|
||||||
|
|
||||||
**Plan**: Make the "full" variants delegate to the "standard" variants by normalizing their option keys once at the call site. Keep only one code path for each (compile/link). |
|
||||||
|
|
||||||
**Expected savings**: ~100+ lines, eliminates drift between the two variants. |
|
||||||
|
|
||||||
### 3.4 Deduplicate Preprocessor AST switch |
|
||||||
|
|
||||||
**File**: `Preprocessor.php` |
|
||||||
|
|
||||||
`prepareFile()` (lines 115-151) and `prepareNamespace()` (lines 196-223) both switch over the same set of AST `Stmt_*` types with nearly identical case bodies. |
|
||||||
|
|
||||||
**Plan**: Extract `processStmt(Node $v)` method that both callers share. |
|
||||||
|
|
||||||
**Expected savings**: ~25 lines. |
|
||||||
|
|
||||||
### 3.5 Remove dead code: `ScopeContext` |
|
||||||
|
|
||||||
**File**: `Context/ScopeContext.php` (7 lines) |
|
||||||
|
|
||||||
An empty class with no properties or methods. Used only as a type annotation in `FunctionContext`. Either populate it with scope-relevant state, or remove it and use plain `\stdClass` / array / null. |
|
||||||
|
|
||||||
### 3.6 Reduce StdContainerTrait coupling |
|
||||||
|
|
||||||
**File**: `Parser/StdContainerTrait.php` (823 lines, 48 methods) |
|
||||||
|
|
||||||
This is effectively a standalone subsystem for std container handling. As a trait, it has unrestricted access to CompilerBase's internals. |
|
||||||
|
|
||||||
**Plan**: Extract core logic into `StdContainerHandler` service class. The trait becomes a thin facade that delegates to the handler. |
|
||||||
|
|
||||||
**Expected savings**: Better testability, clearer boundaries, easier to understand. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Phase 4: Longer-Term Architectural (P3) |
|
||||||
|
|
||||||
### 4.1 Break CompilerBase into domain-specific classes |
|
||||||
|
|
||||||
Currently CompilerBase is a 5,917-line god class using 20 traits as a workaround for PHP's single inheritance. Consider: |
|
||||||
|
|
||||||
- `ExpressionCompiler` — all parseExpr sub-dispatch (~500 lines) |
|
||||||
- `StatementCompiler` — parseStmts, parseIf, parseWhile, parseFor, parseSwitch, etc. |
|
||||||
- `TypeResolver` — parseTypeDecl, detectClassOfExpr, type checking |
|
||||||
- `CallResolver` — parseFuncCall, parseMethodCall, parseStaticCall, parseNew |
|
||||||
|
|
||||||
These would be injected services rather than traits, making CompilerBase a coordinator. |
|
||||||
|
|
||||||
### 4.2 Shared AST walker pattern with Python Translator |
|
||||||
|
|
||||||
Both PHP and Python translators implement the same "walk-collect-indent-emit" pipeline independently. `Core\Translator` could define a standard `walkAst($nodes, callable $visitor)` that handles indentation and line collection. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Implementation Order & Impact Matrix |
|
||||||
|
|
||||||
| # | Item | Savings | Risk | Effort | |
|
||||||
|---|------|---------|------|--------| |
|
||||||
| 1.1 | UnixPlatform base class | ~180 dup lines | Low | 2-3h | |
|
||||||
| 1.2 | GccLikeBackend base class | ~250 dup lines | Medium | 3-4h | |
|
||||||
| 2.1 | BigNum dispatch consolidation | ~40 lines | Low | 1h | |
|
||||||
| 2.2 | Data-driven op dispatch | ~200 lines | Low | 1-2h | |
|
||||||
| 2.3 | Unify call dispatch patterns | ~40 lines | Low | 1-2h | |
|
||||||
| 2.4 | UNIVERSAL_METHODS math dedup | ~25 lines | Low | 30m | |
|
||||||
| 2.5 | Fold method template | ~50 lines | Low | 1h | |
|
||||||
| 2.6 | MSVC compat flags in Clang | ~24 lines | Low | 30m | |
|
||||||
| 2.7 | Merge return-check blocks | ~10 lines | Low | 30m | |
|
||||||
| 2.8 | Fix buildCCompileCommand drift | bug fix | Low | 30m | |
|
||||||
| 3.1 | HasFlags trait | clarity | Low | 1h | |
|
||||||
| 3.2 | CompilerTestCase base class | boilerplate | Low | 1h | |
|
||||||
| 3.3 | Eliminate Full*Options duality | ~100 lines | Medium | 2h | |
|
||||||
| 3.4 | Preprocessor AST switch dedup | ~25 lines | Low | 1h | |
|
||||||
| 3.5 | Remove dead ScopeContext | 7 lines | Low | 15m | |
|
||||||
| 3.6 | StdContainer service class | boundary | Medium | 3-4h | |
|
||||||
| 4.1 | Domain classes | architecture | High | 1-2 weeks | |
|
||||||
| 4.2 | AST walker pattern | architecture | Medium | 3-5h | |
|
||||||
|
|
||||||
**Total estimated savings**: ~950+ lines of duplicated / dead code. |
|
||||||
|
|
||||||
**Recommended execution**: Phase 1 → Phase 2 → Phase 3. Items within each phase are independent and can be parallelized. |
|
||||||
@ -1,325 +0,0 @@ |
|||||||
# Encapsulation Review |
|
||||||
|
|
||||||
## Summary |
|
||||||
|
|
||||||
| Metric | Value | |
|
||||||
|--------|-------| |
|
||||||
| Entity classes with all-public fields | 9/9 (100%) | |
|
||||||
| FunctionContext public properties | 26 (all mutable) | |
|
||||||
| CompilerBase private methods | 5/269 (1.9%) | |
|
||||||
| Traits with direct `$this->context->` access | 6 traits, 60+ sites | |
|
||||||
| ScopeContext (dead code) | 7 lines, empty class | |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 1: Entity classes — all-public mutable fields |
|
||||||
|
|
||||||
**Severity**: High. Every entity class exposes all internal state as public writable properties. External code in Preprocessor/CompilerBase directly mutates them. |
|
||||||
|
|
||||||
### 1.1 ClassDef (18 public properties) |
|
||||||
|
|
||||||
`src/Php/Entity/ClassDef.php` |
|
||||||
|
|
||||||
```php |
|
||||||
public array $methods = []; // externally populated: $classDef->properties[$name] = ... |
|
||||||
public array $properties = []; // externally populated |
|
||||||
public array $constants = []; // externally populated |
|
||||||
public array $implements = []; // externally populated |
|
||||||
public string $extends = ''; // externally set: $this->classDef->extends = ... |
|
||||||
public bool $requireCtor = false; |
|
||||||
public bool $enum = false; |
|
||||||
public ?string $enumBackingType = null; |
|
||||||
public array $enumCases = []; |
|
||||||
public array $abstractMethods = []; |
|
||||||
public ?Trait_ $trait = null; |
|
||||||
public array $traitAliases = []; |
|
||||||
public array $traitIgnored = []; |
|
||||||
public int $flags; // no visibility checks, raw bitmask |
|
||||||
public bool $inheritedFromInternalClass = false; |
|
||||||
public string $ctorInit = ''; // mutated during code generation |
|
||||||
public string $ctorClean = ''; // mutated during code generation |
|
||||||
public FunctionContext $propertyContext; // set after construction |
|
||||||
``` |
|
||||||
|
|
||||||
**Issues**: |
|
||||||
- `$properties`, `$methods`, `$constants` — exposed as raw arrays. External code does `$classDef->properties[$name] = $propDef`. No validation that the key matches `$propDef->name`, no type enforcement. |
|
||||||
- `$flags` — raw int, no guarantee it's a valid Modifiers bitmask. |
|
||||||
- `$ctorInit` / `$ctorClean` — mutated by CompilerBase during code generation, not initialization. |
|
||||||
- Property additions use `addMethod()`, `addAbstractMethod()` but array properties are also set directly via `[] =`. |
|
||||||
- `$extends` — set directly as raw string, bypasses `parent::__construct()` which also sets it on ClassLikeDef. |
|
||||||
|
|
||||||
**Recommendation**: |
|
||||||
- Make `$methods`, `$properties`, `$constants` private, expose via `addMethod()`/`getMethod()` (already exists) |
|
||||||
- Make `$flags` private, expose `isAbstract()` (already exists), add `isFinal()`, `isReadonly()` |
|
||||||
- Make `$extends` write-once via `setExtends(string)` with validation |
|
||||||
- Add `appendCtorInit(string)` and `appendCtorClean(string)` methods instead of direct string mutation |
|
||||||
|
|
||||||
### 1.2 FunctionDef (12 public properties) |
|
||||||
|
|
||||||
`src/Php/Entity/FunctionDef.php` |
|
||||||
|
|
||||||
```php |
|
||||||
public string $name; |
|
||||||
public string $returnType; |
|
||||||
public array $argInfoList = []; // externally populated: $functionDef->argInfoList[] = $argInfo |
|
||||||
public int $argCountRequired = 0; |
|
||||||
public string $params = ''; // generated C++ param string, mutated during compilation |
|
||||||
public string $namespace; |
|
||||||
public bool $method = false; |
|
||||||
public bool $stub = false; |
|
||||||
public bool $returnTypeUndeclared = false; |
|
||||||
public string $returnClass = ''; |
|
||||||
public ?array $returnTypeCheck = null; |
|
||||||
public string $returnTypeStr = ''; |
|
||||||
public ?NodeAbstract $returnTypeNode = null; |
|
||||||
``` |
|
||||||
|
|
||||||
**Issues**: |
|
||||||
- `$name` and `$namespace` are set in constructor but still publicly writable — should be readonly |
|
||||||
- `$argInfoList[]` is directly appended to by Preprocessor (line 329) |
|
||||||
- `$params` is a codegen artifact stored on the entity — belongs in a separate compilation context |
|
||||||
|
|
||||||
**Recommendation**: |
|
||||||
- Make constructor-set properties readonly (`$name`, `$namespace`, `$returnType`) |
|
||||||
- Add `addArg(ArgInfo $arg)` method instead of direct array mutation |
|
||||||
- Extract `$params` to a compilation context separate from the definition entity |
|
||||||
|
|
||||||
### 1.3 PropertyDef (7 public properties) |
|
||||||
|
|
||||||
`src/Php/Entity/PropertyDef.php` |
|
||||||
|
|
||||||
```php |
|
||||||
public string $name; |
|
||||||
public string $type; |
|
||||||
public int $flags; |
|
||||||
public ?string $default = null; |
|
||||||
public ?ArrayInitPlan $arrayInitPlan = null; |
|
||||||
public bool $nullable = false; |
|
||||||
public string $class = ''; // set after construction |
|
||||||
``` |
|
||||||
|
|
||||||
**Issues**: |
|
||||||
- `$class` is set after construction externally (`$propDef->class = $fullClassName`) |
|
||||||
- `$flags` is raw int — already has `isPrivate()`/`isProtected()`/`isPublic()`/`isStatic()` methods, good |
|
||||||
- Constructor already sets all core fields — `$class` should be added to the constructor |
|
||||||
|
|
||||||
**Recommendation**: |
|
||||||
- Add `$class` to the constructor (it's always known at construction time) |
|
||||||
- Make constructor-set fields readonly or private |
|
||||||
|
|
||||||
### 1.4 MethodDef (4 public properties) |
|
||||||
|
|
||||||
`src/Php/Entity/MethodDef.php` |
|
||||||
|
|
||||||
```php |
|
||||||
public int $flags; |
|
||||||
public string $name; |
|
||||||
public ?FunctionDef $functionDef = null; // set after construction |
|
||||||
public bool $hasDynamicCall = false; |
|
||||||
``` |
|
||||||
|
|
||||||
**Issues**: |
|
||||||
- No flag-check methods — inline checks in CompilerBase should use `$methodDef->isPrivate()` instead |
|
||||||
- `$functionDef` is set externally: `$this->methodDef->functionDef = $functionDef` (Preprocessor line 411) |
|
||||||
|
|
||||||
**Recommendation**: |
|
||||||
- Add `HasFlags` trait (from Phase 3.1 of reuse plan) |
|
||||||
- Add `setFunctionDef(FunctionDef $fd)` method with validation |
|
||||||
|
|
||||||
### 1.5 ConstantDef (8 public properties) |
|
||||||
|
|
||||||
```php |
|
||||||
public string $name; |
|
||||||
public string $type; |
|
||||||
public int $flags; |
|
||||||
public string $value; |
|
||||||
public string $arrayExpr = ''; |
|
||||||
public string $class = ''; |
|
||||||
public ?NodeAbstract $valueExpr = null; |
|
||||||
``` |
|
||||||
|
|
||||||
**Issues**: Same pattern — constructor sets core fields, but `$class` is set externally afterward. |
|
||||||
|
|
||||||
**Recommendation**: Add `$class` to constructor. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 2: FunctionContext — public mutable grab-bag |
|
||||||
|
|
||||||
**Severity**: High. 26 public properties, all writable by any code with access to the context object. |
|
||||||
|
|
||||||
`src/Php/Context/FunctionContext.php` |
|
||||||
|
|
||||||
```php |
|
||||||
public ?SsaBuilder $ssaBuilder = null; // transient analysis state |
|
||||||
public array $stableObjects = []; // SSA optimizer state |
|
||||||
public array $hoistedProps = []; // SSA optimizer state |
|
||||||
public array $unsafeObjectProps = []; // SSA optimizer state |
|
||||||
public array $objects = []; // object variable tracking |
|
||||||
public array $stdArrays = []; // std container tracking |
|
||||||
public array $stdContainers = []; // std container tracking |
|
||||||
public array $localVars = []; // local variable table |
|
||||||
public array $staticVars = []; // static variable table |
|
||||||
public array $globalVars = []; // global variable table |
|
||||||
public array $ceWrappers = []; // class entry wrappers |
|
||||||
public int $tmpVarIndex = 0; // auto-increment counter |
|
||||||
public array $arguments = []; // function arguments |
|
||||||
public bool $inLoop = false; // control-flow state |
|
||||||
public bool $inClosure = false; // control-flow state |
|
||||||
public bool $hasMultiLevelBreak = false; |
|
||||||
public bool $hasMultiLevelContinue = false; |
|
||||||
public bool $inAssignExpr = false; // expression context |
|
||||||
public array $beforeStmtLines = []; // deferred code (flushed before stmts) |
|
||||||
public array $afterStmtLines = []; // deferred code (flushed after stmts) |
|
||||||
public array $objectProps; // (uninitialized!) |
|
||||||
public array $staticPropRefs = []; // static property references |
|
||||||
public int $scopeLevel = 0; // lexical scope depth |
|
||||||
/** @var array<int, ScopeContext> */ |
|
||||||
public array $scopeLayouts = []; // per-scope data |
|
||||||
``` |
|
||||||
|
|
||||||
**Issues**: |
|
||||||
- Traits directly mutate deeply nested state: `$this->context->stdArrays[$var] = ...`, `$this->context->localVars[$name] = ...` |
|
||||||
- No semantic grouping — analysis state, variable tracking, control-flow flags all mixed |
|
||||||
- `$objectProps` is declared but never initialized (could be null at runtime) |
|
||||||
- `$scopeLayouts` is managed through `enterScope()`/`leaveScope()` — but can be bypassed |
|
||||||
- `$tmpVarIndex` auto-increment — should use a method instead of direct `++` |
|
||||||
|
|
||||||
**Recommendation**: |
|
||||||
- Group related properties into sub-objects: `VariableTable`, `ControlFlowState`, `ScopeManager` |
|
||||||
- Make properties that should only be read by the compiler layer private/protected with getters |
|
||||||
- Add `incrementTmpVar(): int`, `addLocalVar()`, `addBeforeStmt()` methods |
|
||||||
- Initialize `$objectProps = []` |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 3: CompilerBase — only 1.9% private methods |
|
||||||
|
|
||||||
**Severity**: Medium. Virtually everything is public or protected. |
|
||||||
|
|
||||||
`src/Php/CompilerBase.php` — 269 methods total: |
|
||||||
- ~25 public methods (many should be protected or internal) |
|
||||||
- ~239 protected methods (most should be private — internal helpers) |
|
||||||
- **5 private methods** (1.9%) |
|
||||||
|
|
||||||
### 3.1 Methods that should be private |
|
||||||
|
|
||||||
The following methods are internal helpers only called from within CompilerBase (not from Preprocessor, Translator, or traits). They are unnecessarily `protected`: |
|
||||||
|
|
||||||
| Method | Line | Called from | |
|
||||||
|--------|------|-------------| |
|
||||||
| `resetFunction()` | 833 | Internal only | |
|
||||||
| `resetMethod()` | 840 | Internal only | |
|
||||||
| `resetClass()` | 846 | Internal only | |
|
||||||
| `resolveObjectClassDef()` | 816 | Already private ✓ | |
|
||||||
| `getBigIntLiteralString()` | 1183 | Already private ✓ | |
|
||||||
| `getDecimalLiteralString()` | 1188 | Already private ✓ | |
|
||||||
| `parseBeforeStmtLines()` | 1321 | Internal, but accessed by traits | |
|
||||||
| `parseAfterStmtLines()` | 1331 | Internal, but accessed by traits | |
|
||||||
| `genTmpVarName()` | 734 | Public — should at least be protected | |
|
||||||
|
|
||||||
### 3.2 Public methods that are internal concern |
|
||||||
|
|
||||||
| Method | Current visibility | Issue | |
|
||||||
|--------|-------------------|-------| |
|
||||||
| `genTmpVarName()` | public | Only used internally for variable name generation | |
|
||||||
| `writeFile()` | public | File I/O — should be a separate service | |
|
||||||
| `stop()` | public | Error helper — could be internal | |
|
||||||
| `isScalarInt()` | public | AST helper, only used internally | |
|
||||||
| `getType()` | public | AST helper, only used internally | |
|
||||||
| `getObjectType()` | public | Type mapping, used internally | |
|
||||||
| `getTypeFromZendType()` | public | Type mapping, used internally | |
|
||||||
| `getIncludeDir()` | public | Config getter — should be on a Config object | |
|
||||||
| `getBuildDir()` | public | Config getter — should be on a Config object | |
|
||||||
|
|
||||||
### 3.3 Public constants leaked as API |
|
||||||
|
|
||||||
30 public constants for internal type names, literal values, etc. These are needed by traits but expose internal naming conventions. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 4: Trait → Context coupling |
|
||||||
|
|
||||||
**Severity**: Medium. Traits bypass any encapsulation boundary and directly mutate `$this->context`. |
|
||||||
|
|
||||||
| Trait | `$this->context->` accesses | |
|
||||||
|-------|---------------------------| |
|
||||||
| `StdContainerTrait` | 40+ accesses to `stdArrays`, `stdContainers`, `objects`, `localVars` | |
|
||||||
| `LoopVarOptimizer` | accesses to `localVars`, `arguments`, `scopeLevel`, `inLoop` | |
|
||||||
| `SsaPropOptimizer` | accesses to `stableObjects`, `hoistedProps`, `unsafeObjectProps`, `objects` | |
|
||||||
| `FuncCallOptimizer` | accesses to `beforeStmtLines`, `arguments`, `localVars` | |
|
||||||
| `SsaTypeOptimizer` | accesses to `localVars` | |
|
||||||
| `BinaryOpTrait` | accesses to `objects`, `localVars` | |
|
||||||
|
|
||||||
**Issues**: |
|
||||||
- Traits have no declared contract — they assume `$this->context` exists and has specific properties |
|
||||||
- If a property name changes in FunctionContext, all 6 traits break silently |
|
||||||
- No type safety — arrays are indexed by string but accessed with arbitrary keys |
|
||||||
|
|
||||||
**Recommendation**: |
|
||||||
- Define a `ContextAccess` interface that traits must use instead of direct property access |
|
||||||
- Or: inject context into trait methods as a parameter instead of reading from `$this` |
|
||||||
- Short-term: add `@property-read` annotations to document the contract |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 5: Preprocessor directly mutates entity state |
|
||||||
|
|
||||||
**Severity**: Medium. Preprocessor bypasses entity boundaries. |
|
||||||
|
|
||||||
`src/Php/Preprocessor.php`: |
|
||||||
```php |
|
||||||
line 279: $argInfo->name = $name; // direct property set |
|
||||||
line 329: $functionDef->argInfoList[] = $argInfo; // direct array append |
|
||||||
line 411: $this->methodDef->functionDef = $functionDef; // direct property set |
|
||||||
line 447: $this->classDef->extends = $this->parentClass; // direct property set |
|
||||||
line 585: $this->classDef->constants[$constInfo->name] = ...; // direct array set |
|
||||||
line 618: $this->classDef->properties[$name] = $propDef; // direct array set |
|
||||||
``` |
|
||||||
|
|
||||||
**Recommendation**: Use entity methods: `$functionDef->addArg($argInfo)`, `$this->methodDef->setFunctionDef($functionDef)`, `$classDef->addProperty($propDef)`, etc. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 6: CompilerBase protected state leaked to inheritance chain |
|
||||||
|
|
||||||
**Severity**: Low-Medium. The chain CompilerBase → Preprocessor → Translator means any protected property in CompilerBase is accessible from Translator. |
|
||||||
|
|
||||||
CompilerBase has ~50 protected properties. Translator is 3301 lines and accesses many of them. There's no way to know which properties are "safe to use" vs "internal to CompilerBase." |
|
||||||
|
|
||||||
**Recommendation**: Migrate internal-only properties to `private` over time, with explicit getter methods where needed. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 7: Platform/Backend — well encapsulated |
|
||||||
|
|
||||||
**Severity**: None. The Platform and Backend layers are well-encapsulated: |
|
||||||
- All state is private (e.g., `$compilerCommand`, `$linkerCommand` in GccLikeBackend) |
|
||||||
- Only methods are public |
|
||||||
- Abstract contracts are clear |
|
||||||
- Factory pattern is used consistently |
|
||||||
|
|
||||||
**No changes needed in this layer.** |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Issue 8: ScopeContext is dead code |
|
||||||
|
|
||||||
**Severity**: Low. `src/Php/Context/ScopeContext.php` — 7 lines, empty class body. Used as a placeholder type in FunctionContext's `$scopeLayouts` array. Either populate it or remove it. |
|
||||||
|
|
||||||
--- |
|
||||||
|
|
||||||
## Implementation Priority |
|
||||||
|
|
||||||
| # | Issue | Impact | Effort | Risk | |
|
||||||
|---|-------|--------|--------|------| |
|
||||||
| 1.1 | Entity: readonly for constructor fields | Data integrity | 2h | Low | |
|
||||||
| 1.2 | Entity: add mutation methods (addArg, addProperty, etc.) | Safe mutation | 3h | Medium | |
|
||||||
| 2.1 | FunctionContext: group properties into sub-objects | Clarity | 4h | Medium | |
|
||||||
| 2.2 | FunctionContext: add accessor methods | Controlled mutation | 3h | Medium | |
|
||||||
| 3 | CompilerBase: demote public→protected, protected→private | Boundary clarity | 4h | Medium | |
|
||||||
| 4 | Define trait context contract | Safe coupling | 3h | Medium | |
|
||||||
| 5 | Preprocessor: use entity methods | Consistent mutation | 2h | Low | |
|
||||||
| 8 | Remove ScopeContext dead code | Cleanup | 15m | None | |
|
||||||
|
|
||||||
**Recommended order**: Start with 8 (quick win), then 1.1 + 1.2 (entity cleanup), then 2.1 + 2.2 (context cleanup), then 3 + 5 + 4 (CompilerBase boundary). |
|
||||||
@ -0,0 +1,21 @@ |
|||||||
|
name: Patch PHP headers for C++ |
||||||
|
description: Apply temporary upstream PHP header fixes required by generated C++. |
||||||
|
|
||||||
|
runs: |
||||||
|
using: composite |
||||||
|
steps: |
||||||
|
# Temporary workaround for php/php-src#22935. Some supported PHP packages |
||||||
|
# contain a php_hash.h revision that is valid C but invalid C++. Remove |
||||||
|
# this action when all PHP 8.4/8.5 packages include php/php-src#22940. |
||||||
|
- name: Patch php_hash.h |
||||||
|
shell: bash |
||||||
|
run: | |
||||||
|
php_include_dir="$(php-config --include-dir)" |
||||||
|
php_hash_header="${php_include_dir}/ext/hash/php_hash.h" |
||||||
|
|
||||||
|
if grep -Fq 'char *base = ecalloc(' "${php_hash_header}"; then |
||||||
|
sudo patch --directory="${php_include_dir}" --strip=1 \ |
||||||
|
< "${GITHUB_ACTION_PATH}/../../patches/php-hash-cxx.patch" |
||||||
|
else |
||||||
|
echo "php_hash.h already contains the upstream C++ fix" |
||||||
|
fi |
||||||
@ -1,90 +0,0 @@ |
|||||||
# Copilot instructions for this repository |
|
||||||
|
|
||||||
## Project overview |
|
||||||
|
|
||||||
TypePHP is a PHP native compilation project. Its `tpc` command is TypePHP Compiler (AOT), which translates PHP source into C++, then compiles and links it into a native binary or a PHP extension. The primary entrypoint boots `src/compiler.php`; that drives `TypePhp\Translator` through a fixed pipeline: |
|
||||||
|
|
||||||
1. `prepare()` scans files, parses ASTs, collects symbols, and topologically sorts PHP files by cross-file symbol usage. |
|
||||||
2. `convert()` turns PHP ASTs into generated `.cc` files while passing through native source files (`.cpp`, `.c`, `.s`, `.m`, `.mm`). |
|
||||||
3. `compile()` chooses the platform/compiler backend, generates support sources and headers, and compiles sources, using `pcntl` parallelism when available. |
|
||||||
4. `build()` links object files into the final executable or extension. |
|
||||||
|
|
||||||
`src/CompilerBase.php` contains most PHP-to-C++ translation logic and mixes in many traits for syntax handling and optimizations. `src/Preprocessor.php` owns dependency discovery and file ordering. Platform-specific behavior lives under `src/Platform/`, compiler backends under `src/Backend/`, and metadata/state objects under `src/Entity/` and `src/Context/`. |
|
||||||
|
|
||||||
## Setup and build commands |
|
||||||
|
|
||||||
The repo expects PHP 8.4+, GCC 9+ with C++17, CMake 3.24+, and a compiled `swoole/phpx` dependency. Install PHP dependencies with: |
|
||||||
|
|
||||||
```bash |
|
||||||
composer install |
|
||||||
``` |
|
||||||
|
|
||||||
Build `phpx` before relying on compiler runs: |
|
||||||
|
|
||||||
```bash |
|
||||||
cd vendor/swoole/phpx |
|
||||||
cmake . |
|
||||||
make -j32 |
|
||||||
``` |
|
||||||
|
|
||||||
Compile a project, directory, single file, or `project.yml`: |
|
||||||
|
|
||||||
```bash |
|
||||||
./tpc <path-to-project-or-file> |
|
||||||
./tpc <path> -O2 |
|
||||||
./tpc <path> --mode=ext -o <output_name> |
|
||||||
``` |
|
||||||
|
|
||||||
## Test commands |
|
||||||
|
|
||||||
Run the PHPUnit suite: |
|
||||||
|
|
||||||
```bash |
|
||||||
./vendor/bin/phpunit |
|
||||||
``` |
|
||||||
|
|
||||||
Run a single PHPUnit file or a single test method: |
|
||||||
|
|
||||||
```bash |
|
||||||
./vendor/bin/phpunit phpunit/src/Platform/PlatformTest.php |
|
||||||
./vendor/bin/phpunit --filter testWindowsBasic phpunit/src/Platform/PlatformTest.php |
|
||||||
``` |
|
||||||
|
|
||||||
Run PHPT integration tests: |
|
||||||
|
|
||||||
```bash |
|
||||||
php run-tests.php tests/compiler/ |
|
||||||
php run-tests.php tests/compiler/arrays.phpt |
|
||||||
``` |
|
||||||
|
|
||||||
For parser/runtime comparison without AOT compilation, there are docs using: |
|
||||||
|
|
||||||
```bash |
|
||||||
php run-tests.php --no-aot tests/compiler/arrow-functions.phpt |
|
||||||
``` |
|
||||||
|
|
||||||
## Formatting |
|
||||||
|
|
||||||
The repo ships a PHP CS Fixer config in `.php-cs-fixer.dist.php`: |
|
||||||
|
|
||||||
```bash |
|
||||||
php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php <path> |
|
||||||
``` |
|
||||||
|
|
||||||
Generated C++ is auto-formatted by the compiler itself when `clang-format` is available. |
|
||||||
|
|
||||||
## Configuration and repository conventions |
|
||||||
|
|
||||||
- `project.yml` is the project-level build config. Important keys include `name`, `build-mode`, `cxx-std`, `cxx-flags`, `ld-flags`, `sources`, `ignore`, and `resource`. |
|
||||||
- Command-line options intentionally override YAML values. `Translator` parses YAML first, then applies CLI arguments last. |
|
||||||
- YAML parsing accepts both hyphenated and underscored variants for several keys, but existing examples use hyphenated names such as `build-mode` and `cxx-std`. |
|
||||||
- In `bin` mode, compiled programs must define `main()`. In `ext` mode they do not. |
|
||||||
- File discovery is mixed-language by design: PHP is translated, while native sources are compiled directly if they appear in configured sources. |
|
||||||
- Generated files are written under `build/`, with generated C++ paths mirroring the source tree and generated headers under `build/include/`. |
|
||||||
- Platform/compiler selection is centralized: `PlatformFactory` detects the OS, and `CompilerFactory` picks the backend (`Gcc`, `Clang`, `Msvc`) with environment/config overrides. |
|
||||||
|
|
||||||
## Test-specific conventions |
|
||||||
|
|
||||||
- PHPUnit tests for compiler internals should use `CompilerTest::create(ROOT_PATH)`, which enables test mode instead of normal fatal exits. |
|
||||||
- `phpunit/bootstrap.php` exposes a `BaseTest::exec()` helper that expects compilation failures to surface as `TypePhp\Exception\TestError`. |
|
||||||
- PHPT end-to-end tests live in `tests/compiler/`; existing guidance and examples generally put executable test logic inside a `main()` function. |
|
||||||
@ -0,0 +1,11 @@ |
|||||||
|
--- a/ext/hash/php_hash.h
|
||||||
|
+++ b/ext/hash/php_hash.h
|
||||||
|
@@ -158,7 +158,7 @@ static inline void *php_hash_alloc_context(const php_hash_ops *ops) {
|
||||||
|
/* Zero out context memory so serialization doesn't expose internals */
|
||||||
|
if (ops->context_align > 0) {
|
||||||
|
size_t align = ops->context_align;
|
||||||
|
- char *base = ecalloc(1, ops->context_size + align);
|
||||||
|
+ char *base = (char *) ecalloc(1, ops->context_size + align);
|
||||||
|
size_t offset = align - ((uintptr_t)base & (align - 1));
|
||||||
|
char *ptr = base + offset;
|
||||||
|
ptr[-1] = (char)offset;
|
||||||
@ -0,0 +1,285 @@ |
|||||||
|
name: tests |
||||||
|
|
||||||
|
on: |
||||||
|
push: |
||||||
|
pull_request: |
||||||
|
workflow_dispatch: |
||||||
|
|
||||||
|
permissions: |
||||||
|
contents: read |
||||||
|
|
||||||
|
concurrency: |
||||||
|
group: tests-${{ github.workflow }}-${{ github.ref }} |
||||||
|
cancel-in-progress: true |
||||||
|
|
||||||
|
env: |
||||||
|
COMPOSER_NO_INTERACTION: 1 |
||||||
|
COMPOSER_PROCESS_TIMEOUT: 0 |
||||||
|
|
||||||
|
jobs: |
||||||
|
build-phpx: |
||||||
|
name: Build PHPX (PHP ${{ matrix.php }}) |
||||||
|
runs-on: ubuntu-latest |
||||||
|
timeout-minutes: 30 |
||||||
|
strategy: |
||||||
|
fail-fast: false |
||||||
|
matrix: |
||||||
|
php: ["8.4", "8.5"] |
||||||
|
env: |
||||||
|
PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx |
||||||
|
|
||||||
|
steps: |
||||||
|
- name: Checkout TypePHP |
||||||
|
uses: actions/checkout@v4 |
||||||
|
|
||||||
|
- name: Checkout phpy |
||||||
|
uses: actions/checkout@v4 |
||||||
|
with: |
||||||
|
repository: swoole/phpy |
||||||
|
path: third_party/phpy |
||||||
|
|
||||||
|
- name: Setup PHP |
||||||
|
uses: shivammathur/setup-php@v2 |
||||||
|
with: |
||||||
|
php-version: ${{ matrix.php }} |
||||||
|
coverage: none |
||||||
|
ini-values: precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 |
||||||
|
tools: composer:v2 |
||||||
|
|
||||||
|
- name: Patch php_hash.h C++ compatibility |
||||||
|
uses: ./.github/actions/patch-php-headers |
||||||
|
|
||||||
|
- name: Install native build dependencies |
||||||
|
run: | |
||||||
|
sudo apt-get update |
||||||
|
sudo apt-get install --yes build-essential cmake libgmp-dev libmpfr-dev pkg-config python3-dev |
||||||
|
|
||||||
|
- name: Install Composer dependencies |
||||||
|
run: composer install --prefer-dist --no-progress |
||||||
|
|
||||||
|
- name: Build PHPX |
||||||
|
shell: bash |
||||||
|
run: | |
||||||
|
cmake -S "${PHPX_HOME}" -B "${PHPX_HOME}/build" \ |
||||||
|
-D CMAKE_BUILD_TYPE=Release \ |
||||||
|
-D BUILD_TESTS=OFF \ |
||||||
|
-D BUILD_EXT=OFF \ |
||||||
|
-D GITHUB_ACTION=ON |
||||||
|
cmake --build "${PHPX_HOME}/build" --target phpx --parallel 2 |
||||||
|
test -f "${PHPX_HOME}/lib/libphpx.so" |
||||||
|
|
||||||
|
- name: Build phpy |
||||||
|
working-directory: third_party/phpy |
||||||
|
run: | |
||||||
|
phpize |
||||||
|
./configure |
||||||
|
make -j2 |
||||||
|
test -f modules/phpy.so |
||||||
|
|
||||||
|
- name: Upload PHPX library |
||||||
|
uses: actions/upload-artifact@v4 |
||||||
|
with: |
||||||
|
name: libphpx-php-${{ matrix.php }} |
||||||
|
if-no-files-found: error |
||||||
|
retention-days: 1 |
||||||
|
path: vendor/swoole/phpx/lib/libphpx.so |
||||||
|
|
||||||
|
- name: Upload phpy extension |
||||||
|
uses: actions/upload-artifact@v4 |
||||||
|
with: |
||||||
|
name: phpy-php-${{ matrix.php }} |
||||||
|
if-no-files-found: error |
||||||
|
retention-days: 1 |
||||||
|
path: third_party/phpy/modules/phpy.so |
||||||
|
|
||||||
|
phpunit: |
||||||
|
name: PHPUnit (PHP ${{ matrix.php }}) |
||||||
|
runs-on: ubuntu-latest |
||||||
|
timeout-minutes: 30 |
||||||
|
needs: build-phpx |
||||||
|
strategy: |
||||||
|
fail-fast: false |
||||||
|
matrix: |
||||||
|
php: ["8.4", "8.5"] |
||||||
|
env: |
||||||
|
PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx |
||||||
|
|
||||||
|
steps: |
||||||
|
- name: Checkout TypePHP |
||||||
|
uses: actions/checkout@v4 |
||||||
|
|
||||||
|
- name: Setup PHP |
||||||
|
uses: shivammathur/setup-php@v2 |
||||||
|
with: |
||||||
|
php-version: ${{ matrix.php }} |
||||||
|
coverage: none |
||||||
|
extensions: curl, redis, mbstring, ffi |
||||||
|
ini-values: ffi.enable=1, phpy.enable_operator_overloading=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 |
||||||
|
tools: composer:v2 |
||||||
|
|
||||||
|
- name: Patch php_hash.h C++ compatibility |
||||||
|
uses: ./.github/actions/patch-php-headers |
||||||
|
|
||||||
|
- name: Install native build dependencies |
||||||
|
run: | |
||||||
|
sudo apt-get update |
||||||
|
sudo apt-get install --yes build-essential cmake libgmp-dev libmpfr-dev pkg-config python3-dev |
||||||
|
|
||||||
|
- name: Install Composer dependencies |
||||||
|
run: composer install --prefer-dist --no-progress |
||||||
|
|
||||||
|
- name: Download PHPX library |
||||||
|
uses: actions/download-artifact@v4 |
||||||
|
with: |
||||||
|
name: libphpx-php-${{ matrix.php }} |
||||||
|
path: vendor/swoole/phpx/lib |
||||||
|
|
||||||
|
- name: Download phpy extension |
||||||
|
uses: actions/download-artifact@v4 |
||||||
|
with: |
||||||
|
name: phpy-php-${{ matrix.php }} |
||||||
|
path: third_party/phpy/modules |
||||||
|
|
||||||
|
- name: Enable phpy extension |
||||||
|
shell: bash |
||||||
|
run: | |
||||||
|
php_ini_dir="$(php-config --ini-dir)" |
||||||
|
echo "extension=${GITHUB_WORKSPACE}/third_party/phpy/modules/phpy.so" \ |
||||||
|
| sudo tee "${php_ini_dir}/90-phpy.ini" |
||||||
|
echo "PHP_INI_SCAN_DIR=${php_ini_dir}" >> "${GITHUB_ENV}" |
||||||
|
php --ri phpy |
||||||
|
|
||||||
|
- name: Configure native library path |
||||||
|
shell: bash |
||||||
|
run: echo "LD_LIBRARY_PATH=${PHPX_HOME}/lib:$(php-config --prefix)/lib" >> "${GITHUB_ENV}" |
||||||
|
|
||||||
|
- name: Run PHPUnit |
||||||
|
run: vendor/bin/phpunit |
||||||
|
|
||||||
|
phpt: |
||||||
|
name: PHPT (PHP ${{ matrix.php }}) |
||||||
|
runs-on: ubuntu-latest |
||||||
|
timeout-minutes: 180 |
||||||
|
needs: build-phpx |
||||||
|
strategy: |
||||||
|
fail-fast: false |
||||||
|
matrix: |
||||||
|
php: ["8.4", "8.5"] |
||||||
|
env: |
||||||
|
PHPX_HOME: ${{ github.workspace }}/vendor/swoole/phpx |
||||||
|
NO_INTERACTION: 1 |
||||||
|
REPORT_EXIT_STATUS: 1 |
||||||
|
TYPEPHP_PHPT_GENERATED_ARTIFACT_DIR: ${{ github.workspace }}/build/phpt-generated |
||||||
|
|
||||||
|
steps: |
||||||
|
- name: Checkout TypePHP |
||||||
|
uses: actions/checkout@v4 |
||||||
|
|
||||||
|
- name: Setup PHP |
||||||
|
uses: shivammathur/setup-php@v2 |
||||||
|
with: |
||||||
|
php-version: ${{ matrix.php }} |
||||||
|
coverage: none |
||||||
|
extensions: embed, curl, redis, mbstring, ffi |
||||||
|
ini-values: ffi.enable=1, phpy.enable_operator_overloading=0, opcache.jit=0, precision=17, memory_limit=4G, error_reporting=E_ERROR|E_WARNING, display_errors=1, display_startup_errors=1, log_errors=0 |
||||||
|
tools: composer:v2 |
||||||
|
|
||||||
|
- name: Patch php_hash.h C++ compatibility |
||||||
|
uses: ./.github/actions/patch-php-headers |
||||||
|
|
||||||
|
- name: Install native build dependencies |
||||||
|
run: | |
||||||
|
sudo apt-get update |
||||||
|
sudo apt-get install --yes build-essential cmake libgmp-dev libmpfr-dev pkg-config python3-dev |
||||||
|
|
||||||
|
- name: Configure version-matched PHP embed library |
||||||
|
shell: bash |
||||||
|
run: | |
||||||
|
embed_package="libphp${{ matrix.php }}-embed" |
||||||
|
php_version="$(php-config --version)" |
||||||
|
embed_version="$(dpkg-query --show --showformat='${Version}' "${embed_package}")" |
||||||
|
case "${embed_version}" in |
||||||
|
"${php_version}"*) ;; |
||||||
|
*) echo "PHP embed package ${embed_version} does not match PHP ${php_version}" >&2; exit 1 ;; |
||||||
|
esac |
||||||
|
embed_library="$(dpkg-query --listfiles "${embed_package}" \ |
||||||
|
| sed -n '/\/libphp[0-9][^/]*\.so$/ { p; q; }')" |
||||||
|
test -n "${embed_library}" |
||||||
|
test -f "${embed_library}" |
||||||
|
|
||||||
|
php_home="${RUNNER_TEMP}/typephp-php-${{ matrix.php }}" |
||||||
|
mkdir -p "${php_home}/bin" "${php_home}/include" "${php_home}/lib" |
||||||
|
ln -s "$(command -v php-config)" "${php_home}/bin/php-config" |
||||||
|
ln -s "$(php-config --include-dir)" "${php_home}/include/php" |
||||||
|
ln -s "${embed_library}" "${php_home}/lib/libphp.so" |
||||||
|
|
||||||
|
echo "PHP_HOME=${php_home}" >> "${GITHUB_ENV}" |
||||||
|
echo "Using ${embed_library} from ${embed_package} ${embed_version} for PHP ${php_version}" |
||||||
|
|
||||||
|
- name: Install Composer dependencies |
||||||
|
run: composer install --prefer-dist --no-progress |
||||||
|
|
||||||
|
- name: Download PHPX library |
||||||
|
uses: actions/download-artifact@v4 |
||||||
|
with: |
||||||
|
name: libphpx-php-${{ matrix.php }} |
||||||
|
path: vendor/swoole/phpx/lib |
||||||
|
|
||||||
|
- name: Download phpy extension |
||||||
|
uses: actions/download-artifact@v4 |
||||||
|
with: |
||||||
|
name: phpy-php-${{ matrix.php }} |
||||||
|
path: third_party/phpy/modules |
||||||
|
|
||||||
|
- name: Enable phpy extension |
||||||
|
shell: bash |
||||||
|
run: | |
||||||
|
php_ini_dir="$(php-config --ini-dir)" |
||||||
|
echo "extension=${GITHUB_WORKSPACE}/third_party/phpy/modules/phpy.so" \ |
||||||
|
| sudo tee "${php_ini_dir}/90-phpy.ini" |
||||||
|
echo "PHP_INI_SCAN_DIR=${php_ini_dir}" >> "${GITHUB_ENV}" |
||||||
|
php --ri phpy |
||||||
|
|
||||||
|
- name: Configure native library path |
||||||
|
shell: bash |
||||||
|
run: | |
||||||
|
test -f "${PHPX_HOME}/lib/libphpx.so" |
||||||
|
test -f "${PHP_HOME}/lib/libphp.so" |
||||||
|
echo "LD_LIBRARY_PATH=${PHPX_HOME}/lib:${PHP_HOME}/lib" >> "${GITHUB_ENV}" |
||||||
|
|
||||||
|
- name: Build bootstrap TypePHP compiler |
||||||
|
run: | |
||||||
|
php bin/tpc.php project.yml --job 2 --no-progress |
||||||
|
test -x ./tpc |
||||||
|
./tpc --version |
||||||
|
|
||||||
|
- name: Show build environment |
||||||
|
run: | |
||||||
|
php -v |
||||||
|
php --ini |
||||||
|
ldd ./tpc | grep -E 'libphp(x)?[0-9.]*\.so' |
||||||
|
cmake --version |
||||||
|
c++ --version |
||||||
|
|
||||||
|
- name: Run compiler PHPT suite with bootstrap compiler |
||||||
|
run: | |
||||||
|
mkdir -p build |
||||||
|
php run-tests.php -q -j8 --compiler ./tpc \ |
||||||
|
-w build/failed-tests.txt -W build/test-results.txt tests/compiler |
||||||
|
|
||||||
|
- name: Upload PHPT failure artifacts |
||||||
|
if: failure() |
||||||
|
uses: actions/upload-artifact@v4 |
||||||
|
with: |
||||||
|
name: phpt-failures-php-${{ matrix.php }} |
||||||
|
if-no-files-found: ignore |
||||||
|
retention-days: 7 |
||||||
|
path: | |
||||||
|
build/failed-tests.txt |
||||||
|
build/test-results.txt |
||||||
|
build/**/*.cc |
||||||
|
build/**/*.h |
||||||
|
php_test_results_*.txt |
||||||
|
tests/compiler/**/*.diff |
||||||
|
tests/compiler/**/*.log |
||||||
|
tests/compiler/**/*.out |
||||||
@ -1,103 +0,0 @@ |
|||||||
# CLAUDE.md |
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
|
||||||
|
|
||||||
## Project Overview |
|
||||||
|
|
||||||
TypePHP is a PHP native compilation project. Its `tpc` command is TypePHP Compiler (AOT), which translates PHP source code to C++, then compiles it with GCC/Clang/MSVC into native binaries. It supports Linux (primary), macOS, and Windows. |
|
||||||
|
|
||||||
**Prerequisites**: PHP 8.2+, GCC 9+ (C++17), CMake 3.24+. The `swoole/phpx` extension must be compiled (see README.md). |
|
||||||
|
|
||||||
## TypePHP Language Design Principles |
|
||||||
|
|
||||||
TypePHP should not blindly mirror every PHP language behavior. Most PHP syntax and semantics should remain compatible with ZendPHP, but some legal PHP constructs are historical baggage or language-design mistakes that conflict with static compilation, clear semantics, or robust generated C++ code. |
|
||||||
|
|
||||||
When reviewing or changing compiler behavior: |
|
||||||
|
|
||||||
- Prefer PHP compatibility for common, well-defined syntax that does not weaken the TypePHP static model. |
|
||||||
- Reject PHP historical baggage when the syntax is ambiguous, surprising, or only preserved for legacy compatibility. |
|
||||||
- Diagnose such cases as early as possible during preprocessing/static compilation, instead of deferring to runtime TypeCheck or ZendVM errors. |
|
||||||
- Provide precise errors that include the relevant function/method name, parameter/property name, and type information where applicable. |
|
||||||
- Compare with other statically compiled languages such as C/C++, Java, C#, Go, Rust, Kotlin, and TypeScript before deciding whether TypePHP should preserve or reject a PHP behavior. |
|
||||||
|
|
||||||
Example: `function test($a = 1, $b, $c) {}` is legal in PHP, but the default value for `$a` is effectively ignored and all parameters become required. This is a PHP historical compatibility artifact. TypePHP should reject it during preprocessing instead of preserving the behavior. |
|
||||||
|
|
||||||
Example: PHP permits `return $value;` inside `__construct()` and lets callers consume `parent::__construct()` as a value, even though constructors cannot declare a return type. TypePHP treats constructors consistently with C++/Java-style semantics: constructors initialize objects and must not return values. `return;` is allowed, but `return $value;` or using a constructor call as a value must be rejected during static compilation. |
|
||||||
|
|
||||||
## Build & Test Commands |
|
||||||
|
|
||||||
```bash |
|
||||||
# Install PHP dependencies |
|
||||||
composer install |
|
||||||
|
|
||||||
# Compile a PHP project to a native binary |
|
||||||
./tpc <path-to-project-or-file> |
|
||||||
|
|
||||||
# Run all PHPUnit tests |
|
||||||
./vendor/bin/phpunit |
|
||||||
|
|
||||||
# Run a single PHPUnit test class |
|
||||||
./vendor/bin/phpunit phpunit/src/AstNodeTypeTest.php |
|
||||||
|
|
||||||
# Run PHPT integration tests (all) |
|
||||||
php run-tests.php tests/compiler/ |
|
||||||
|
|
||||||
# Run a single PHPT test |
|
||||||
php run-tests.php tests/compiler/arrays.phpt |
|
||||||
``` |
|
||||||
|
|
||||||
## Architecture |
|
||||||
|
|
||||||
### Translation Pipeline |
|
||||||
|
|
||||||
The compiler follows a 4-stage pipeline, orchestrated by `src/Translator.php` (the main entry point): |
|
||||||
|
|
||||||
1. **prepare()** — Scan PHP files, collect symbol declarations and dependencies, topological-sort for compilation order |
|
||||||
2. **convert()** — Parse PHP AST via `nikic/php-parser`, translate each node to C++ source code |
|
||||||
3. **compile()** — Invoke the platform C++ compiler (GCC/Clang/MSVC) on generated `.cc` files |
|
||||||
4. **build()** — Link object files into a native binary executable |
|
||||||
|
|
||||||
### Class Hierarchy |
|
||||||
|
|
||||||
``` |
|
||||||
src/CompilerBase.php (core PHP→C++ translation logic, indent/output/mode helpers) |
|
||||||
├─ uses traits: AstNodeType, FuncCallOptimizer, ClosureGenerator, |
|
||||||
│ PlaceHolderGenerator, PropertyPromotion, MagicMethodDetector |
|
||||||
└─ src/Preprocessor.php (scanning, symbol tables, dependency sort, YAML config) |
|
||||||
└─ src/Translator.php (full pipeline: prepare→convert→compile→build) |
|
||||||
└─ src/CompilerTest.php (test-only subclass, used by PHPUnit tests) |
|
||||||
``` |
|
||||||
|
|
||||||
### Key Components |
|
||||||
|
|
||||||
| Directory | Purpose | |
|
||||||
|-----------|---------| |
|
||||||
| `src/Entity/` | Data classes: `ClassDef`, `FunctionDef`, `MethodDef`, `PropertyDef`, `ConstantDef`, `InterfaceDef` | |
|
||||||
| `src/Generator/` | Codegen helpers: `ClosureGenerator`, `PlaceHolderGenerator`, `PropertyPromotion`, `Utils` | |
|
||||||
| `src/Backend/` | Compiler abstraction: `CompilerBackend` (abstract) → `Gcc`, `Clang`, `Msvc`. Factory pattern via `CompilerFactory` | |
|
||||||
| `src/Platform/` | OS abstraction: `PlatformBase` → `Linux`, `Macos`, `Windows`. Factory via `PlatformFactory` | |
|
||||||
| `src/Context/` | `ScopeContext` and `FunctionContext` for variable scoping and type tracking | |
|
||||||
| `src/Exception/` | `SyntaxError`, `Unsupported`, `DynamicCall`, `PlaceHolder`, `Skip`, `Redo`, `TestError` | |
|
||||||
| `src/Parser/` | Special-purpose parsers like `StdContainerParser` (C++ std container foreach support) | |
|
||||||
| `src/Resolver/Reflection.php` | Static helpers wrapping PHP reflection (internal class/function detection) | |
|
||||||
| `src/Generator/Symbol.php` | Maps PHP operations to `phpx` C++ API symbol names | |
|
||||||
| `src/Build/FileScanner.php` | Recursive file discovery with extension filtering (supports `.php`, `.cpp`, `.c`, `.s`, `.m`, `.mm`) | |
|
||||||
| `src/Entity/ArgInfo.php` | Generates C function argument info structures for internal function registration | |
|
||||||
| `src/Extractor.php` | Extracts interfaces from PHP classes | |
|
||||||
| `src/Transform/Visitor.php` | Base `NodeVisitorAbstract` extension (skeleton for custom AST visitors) | |
|
||||||
|
|
||||||
### Configuration |
|
||||||
|
|
||||||
- `project.yml` — per-project build config (name, build-mode, C++ standard, compiler flags, sources, resources/icon) |
|
||||||
- Command-line arguments and YAML config are merged in `Preprocessor`, with CLI taking highest priority |
|
||||||
|
|
||||||
### Generated Output |
|
||||||
|
|
||||||
Generated `.cc` and `.o` files land in `build/` directory. The compiled binary is named from `project.yml`'s `name` field (default: `app`). |
|
||||||
|
|
||||||
### Test Infrastructure |
|
||||||
|
|
||||||
- **PHPUnit tests** (`phpunit/src/`) — unit/integration tests for compiler internals. Bootstrap at `phpunit/bootstrap.php` defines a `BaseTest` class with an `exec()` helper that runs the compiler and expects a `TestError` exception containing a given string |
|
||||||
- **PHPT tests** (`tests/compiler/`) — end-to-end tests using the standard PHPT format (`run-tests.php`). Each `.phpt` contains PHP source and expected output sections |
|
||||||
|
|
||||||
When writing new compiler tests, use `CompilerTest::create(ROOT_PATH)` (in `src/CompilerTest.php`) which sets `forTest = true` to enable test-specific behavior without writing files to disk. |
|
||||||
@ -0,0 +1,674 @@ |
|||||||
|
GNU GENERAL PUBLIC LICENSE |
||||||
|
Version 3, 29 June 2007 |
||||||
|
|
||||||
|
Copyright (C) 2026 上海识沃网络科技有限公司. <https://www.swoole.com/> |
||||||
|
Everyone is permitted to copy and distribute verbatim copies |
||||||
|
of this license document, but changing it is not allowed. |
||||||
|
|
||||||
|
Preamble |
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for |
||||||
|
software and other kinds of works. |
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed |
||||||
|
to take away your freedom to share and change the works. By contrast, |
||||||
|
the GNU General Public License is intended to guarantee your freedom to |
||||||
|
share and change all versions of a program--to make sure it remains free |
||||||
|
software for all its users. We, the Free Software Foundation, use the |
||||||
|
GNU General Public License for most of our software; it applies also to |
||||||
|
any other work released this way by its authors. You can apply it to |
||||||
|
your programs, too. |
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not |
||||||
|
price. Our General Public Licenses are designed to make sure that you |
||||||
|
have the freedom to distribute copies of free software (and charge for |
||||||
|
them if you wish), that you receive source code or can get it if you |
||||||
|
want it, that you can change the software or use pieces of it in new |
||||||
|
free programs, and that you know you can do these things. |
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you |
||||||
|
these rights or asking you to surrender the rights. Therefore, you have |
||||||
|
certain responsibilities if you distribute copies of the software, or if |
||||||
|
you modify it: responsibilities to respect the freedom of others. |
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether |
||||||
|
gratis or for a fee, you must pass on to the recipients the same |
||||||
|
freedoms that you received. You must make sure that they, too, receive |
||||||
|
or can get the source code. And you must show them these terms so they |
||||||
|
know their rights. |
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps: |
||||||
|
(1) assert copyright on the software, and (2) offer you this License |
||||||
|
giving you legal permission to copy, distribute and/or modify it. |
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains |
||||||
|
that there is no warranty for this free software. For both users' and |
||||||
|
authors' sake, the GPL requires that modified versions be marked as |
||||||
|
changed, so that their problems will not be attributed erroneously to |
||||||
|
authors of previous versions. |
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run |
||||||
|
modified versions of the software inside them, although the manufacturer |
||||||
|
can do so. This is fundamentally incompatible with the aim of |
||||||
|
protecting users' freedom to change the software. The systematic |
||||||
|
pattern of such abuse occurs in the area of products for individuals to |
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we |
||||||
|
have designed this version of the GPL to prohibit the practice for those |
||||||
|
products. If such problems arise substantially in other domains, we |
||||||
|
stand ready to extend this provision to those domains in future versions |
||||||
|
of the GPL, as needed to protect the freedom of users. |
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents. |
||||||
|
States should not allow patents to restrict development and use of |
||||||
|
software on general-purpose computers, but in those that do, we wish to |
||||||
|
avoid the special danger that patents applied to a free program could |
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that |
||||||
|
patents cannot be used to render the program non-free. |
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and |
||||||
|
modification follow. |
||||||
|
|
||||||
|
TERMS AND CONDITIONS |
||||||
|
|
||||||
|
0. Definitions. |
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License. |
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of |
||||||
|
works, such as semiconductor masks. |
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this |
||||||
|
License. Each licensee is addressed as "you". "Licensees" and |
||||||
|
"recipients" may be individuals or organizations. |
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work |
||||||
|
in a fashion requiring copyright permission, other than the making of an |
||||||
|
exact copy. The resulting work is called a "modified version" of the |
||||||
|
earlier work or a work "based on" the earlier work. |
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based |
||||||
|
on the Program. |
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without |
||||||
|
permission, would make you directly or secondarily liable for |
||||||
|
infringement under applicable copyright law, except executing it on a |
||||||
|
computer or modifying a private copy. Propagation includes copying, |
||||||
|
distribution (with or without modification), making available to the |
||||||
|
public, and in some countries other activities as well. |
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other |
||||||
|
parties to make or receive copies. Mere interaction with a user through |
||||||
|
a computer network, with no transfer of a copy, is not conveying. |
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices" |
||||||
|
to the extent that it includes a convenient and prominently visible |
||||||
|
feature that (1) displays an appropriate copyright notice, and (2) |
||||||
|
tells the user that there is no warranty for the work (except to the |
||||||
|
extent that warranties are provided), that licensees may convey the |
||||||
|
work under this License, and how to view a copy of this License. If |
||||||
|
the interface presents a list of user commands or options, such as a |
||||||
|
menu, a prominent item in the list meets this criterion. |
||||||
|
|
||||||
|
1. Source Code. |
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work |
||||||
|
for making modifications to it. "Object code" means any non-source |
||||||
|
form of a work. |
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official |
||||||
|
standard defined by a recognized standards body, or, in the case of |
||||||
|
interfaces specified for a particular programming language, one that |
||||||
|
is widely used among developers working in that language. |
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other |
||||||
|
than the work as a whole, that (a) is included in the normal form of |
||||||
|
packaging a Major Component, but which is not part of that Major |
||||||
|
Component, and (b) serves only to enable use of the work with that |
||||||
|
Major Component, or to implement a Standard Interface for which an |
||||||
|
implementation is available to the public in source code form. A |
||||||
|
"Major Component", in this context, means a major essential component |
||||||
|
(kernel, window system, and so on) of the specific operating system |
||||||
|
(if any) on which the executable work runs, or a compiler used to |
||||||
|
produce the work, or an object code interpreter used to run it. |
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all |
||||||
|
the source code needed to generate, install, and (for an executable |
||||||
|
work) run the object code and to modify the work, including scripts to |
||||||
|
control those activities. However, it does not include the work's |
||||||
|
System Libraries, or general-purpose tools or generally available free |
||||||
|
programs which are used unmodified in performing those activities but |
||||||
|
which are not part of the work. For example, Corresponding Source |
||||||
|
includes interface definition files associated with source files for |
||||||
|
the work, and the source code for shared libraries and dynamically |
||||||
|
linked subprograms that the work is specifically designed to require, |
||||||
|
such as by intimate data communication or control flow between those |
||||||
|
subprograms and other parts of the work. |
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users |
||||||
|
can regenerate automatically from other parts of the Corresponding |
||||||
|
Source. |
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that |
||||||
|
same work. |
||||||
|
|
||||||
|
2. Basic Permissions. |
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of |
||||||
|
copyright on the Program, and are irrevocable provided the stated |
||||||
|
conditions are met. This License explicitly affirms your unlimited |
||||||
|
permission to run the unmodified Program. The output from running a |
||||||
|
covered work is covered by this License only if the output, given its |
||||||
|
content, constitutes a covered work. This License acknowledges your |
||||||
|
rights of fair use or other equivalent, as provided by copyright law. |
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not |
||||||
|
convey, without conditions so long as your license otherwise remains |
||||||
|
in force. You may convey covered works to others for the sole purpose |
||||||
|
of having them make modifications exclusively for you, or provide you |
||||||
|
with facilities for running those works, provided that you comply with |
||||||
|
the terms of this License in conveying all material for which you do |
||||||
|
not control copyright. Those thus making or running the covered works |
||||||
|
for you must do so exclusively on your behalf, under your direction |
||||||
|
and control, on terms that prohibit them from making any copies of |
||||||
|
your copyrighted material outside their relationship with you. |
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under |
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10 |
||||||
|
makes it unnecessary. |
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law. |
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological |
||||||
|
measure under any applicable law fulfilling obligations under article |
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or |
||||||
|
similar laws prohibiting or restricting circumvention of such |
||||||
|
measures. |
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid |
||||||
|
circumvention of technological measures to the extent such circumvention |
||||||
|
is effected by exercising rights under this License with respect to |
||||||
|
the covered work, and you disclaim any intention to limit operation or |
||||||
|
modification of the work as a means of enforcing, against the work's |
||||||
|
users, your or third parties' legal rights to forbid circumvention of |
||||||
|
technological measures. |
||||||
|
|
||||||
|
4. Conveying Verbatim Copies. |
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you |
||||||
|
receive it, in any medium, provided that you conspicuously and |
||||||
|
appropriately publish on each copy an appropriate copyright notice; |
||||||
|
keep intact all notices stating that this License and any |
||||||
|
non-permissive terms added in accord with section 7 apply to the code; |
||||||
|
keep intact all notices of the absence of any warranty; and give all |
||||||
|
recipients a copy of this License along with the Program. |
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey, |
||||||
|
and you may offer support or warranty protection for a fee. |
||||||
|
|
||||||
|
5. Conveying Modified Source Versions. |
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to |
||||||
|
produce it from the Program, in the form of source code under the |
||||||
|
terms of section 4, provided that you also meet all of these conditions: |
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified |
||||||
|
it, and giving a relevant date. |
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is |
||||||
|
released under this License and any conditions added under section |
||||||
|
7. This requirement modifies the requirement in section 4 to |
||||||
|
"keep intact all notices". |
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this |
||||||
|
License to anyone who comes into possession of a copy. This |
||||||
|
License will therefore apply, along with any applicable section 7 |
||||||
|
additional terms, to the whole of the work, and all its parts, |
||||||
|
regardless of how they are packaged. This License gives no |
||||||
|
permission to license the work in any other way, but it does not |
||||||
|
invalidate such permission if you have separately received it. |
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display |
||||||
|
Appropriate Legal Notices; however, if the Program has interactive |
||||||
|
interfaces that do not display Appropriate Legal Notices, your |
||||||
|
work need not make them do so. |
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent |
||||||
|
works, which are not by their nature extensions of the covered work, |
||||||
|
and which are not combined with it such as to form a larger program, |
||||||
|
in or on a volume of a storage or distribution medium, is called an |
||||||
|
"aggregate" if the compilation and its resulting copyright are not |
||||||
|
used to limit the access or legal rights of the compilation's users |
||||||
|
beyond what the individual works permit. Inclusion of a covered work |
||||||
|
in an aggregate does not cause this License to apply to the other |
||||||
|
parts of the aggregate. |
||||||
|
|
||||||
|
6. Conveying Non-Source Forms. |
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms |
||||||
|
of sections 4 and 5, provided that you also convey the |
||||||
|
machine-readable Corresponding Source under the terms of this License, |
||||||
|
in one of these ways: |
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product |
||||||
|
(including a physical distribution medium), accompanied by the |
||||||
|
Corresponding Source fixed on a durable physical medium |
||||||
|
customarily used for software interchange. |
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product |
||||||
|
(including a physical distribution medium), accompanied by a |
||||||
|
written offer, valid for at least three years and valid for as |
||||||
|
long as you offer spare parts or customer support for that product |
||||||
|
model, to give anyone who possesses the object code either (1) a |
||||||
|
copy of the Corresponding Source for all the software in the |
||||||
|
product that is covered by this License, on a durable physical |
||||||
|
medium customarily used for software interchange, for a price no |
||||||
|
more than your reasonable cost of physically performing this |
||||||
|
conveying of source, or (2) access to copy the |
||||||
|
Corresponding Source from a network server at no charge. |
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the |
||||||
|
written offer to provide the Corresponding Source. This |
||||||
|
alternative is allowed only occasionally and noncommercially, and |
||||||
|
only if you received the object code with such an offer, in accord |
||||||
|
with subsection 6b. |
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated |
||||||
|
place (gratis or for a charge), and offer equivalent access to the |
||||||
|
Corresponding Source in the same way through the same place at no |
||||||
|
further charge. You need not require recipients to copy the |
||||||
|
Corresponding Source along with the object code. If the place to |
||||||
|
copy the object code is a network server, the Corresponding Source |
||||||
|
may be on a different server (operated by you or a third party) |
||||||
|
that supports equivalent copying facilities, provided you maintain |
||||||
|
clear directions next to the object code saying where to find the |
||||||
|
Corresponding Source. Regardless of what server hosts the |
||||||
|
Corresponding Source, you remain obligated to ensure that it is |
||||||
|
available for as long as needed to satisfy these requirements. |
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided |
||||||
|
you inform other peers where the object code and Corresponding |
||||||
|
Source of the work are being offered to the general public at no |
||||||
|
charge under subsection 6d. |
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded |
||||||
|
from the Corresponding Source as a System Library, need not be |
||||||
|
included in conveying the object code work. |
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any |
||||||
|
tangible personal property which is normally used for personal, family, |
||||||
|
or household purposes, or (2) anything designed or sold for incorporation |
||||||
|
into a dwelling. In determining whether a product is a consumer product, |
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular |
||||||
|
product received by a particular user, "normally used" refers to a |
||||||
|
typical or common use of that class of product, regardless of the status |
||||||
|
of the particular user or of the way in which the particular user |
||||||
|
actually uses, or expects or is expected to use, the product. A product |
||||||
|
is a consumer product regardless of whether the product has substantial |
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent |
||||||
|
the only significant mode of use of the product. |
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods, |
||||||
|
procedures, authorization keys, or other information required to install |
||||||
|
and execute modified versions of a covered work in that User Product from |
||||||
|
a modified version of its Corresponding Source. The information must |
||||||
|
suffice to ensure that the continued functioning of the modified object |
||||||
|
code is in no case prevented or interfered with solely because |
||||||
|
modification has been made. |
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or |
||||||
|
specifically for use in, a User Product, and the conveying occurs as |
||||||
|
part of a transaction in which the right of possession and use of the |
||||||
|
User Product is transferred to the recipient in perpetuity or for a |
||||||
|
fixed term (regardless of how the transaction is characterized), the |
||||||
|
Corresponding Source conveyed under this section must be accompanied |
||||||
|
by the Installation Information. But this requirement does not apply |
||||||
|
if neither you nor any third party retains the ability to install |
||||||
|
modified object code on the User Product (for example, the work has |
||||||
|
been installed in ROM). |
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a |
||||||
|
requirement to continue to provide support service, warranty, or updates |
||||||
|
for a work that has been modified or installed by the recipient, or for |
||||||
|
the User Product in which it has been modified or installed. Access to a |
||||||
|
network may be denied when the modification itself materially and |
||||||
|
adversely affects the operation of the network or violates the rules and |
||||||
|
protocols for communication across the network. |
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided, |
||||||
|
in accord with this section must be in a format that is publicly |
||||||
|
documented (and with an implementation available to the public in |
||||||
|
source code form), and must require no special password or key for |
||||||
|
unpacking, reading or copying. |
||||||
|
|
||||||
|
7. Additional Terms. |
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this |
||||||
|
License by making exceptions from one or more of its conditions. |
||||||
|
Additional permissions that are applicable to the entire Program shall |
||||||
|
be treated as though they were included in this License, to the extent |
||||||
|
that they are valid under applicable law. If additional permissions |
||||||
|
apply only to part of the Program, that part may be used separately |
||||||
|
under those permissions, but the entire Program remains governed by |
||||||
|
this License without regard to the additional permissions. |
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option |
||||||
|
remove any additional permissions from that copy, or from any part of |
||||||
|
it. (Additional permissions may be written to require their own |
||||||
|
removal in certain cases when you modify the work.) You may place |
||||||
|
additional permissions on material, added by you to a covered work, |
||||||
|
for which you have or can give appropriate copyright permission. |
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you |
||||||
|
add to a covered work, you may (if authorized by the copyright holders of |
||||||
|
that material) supplement the terms of this License with terms: |
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the |
||||||
|
terms of sections 15 and 16 of this License; or |
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or |
||||||
|
author attributions in that material or in the Appropriate Legal |
||||||
|
Notices displayed by works containing it; or |
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or |
||||||
|
requiring that modified versions of such material be marked in |
||||||
|
reasonable ways as different from the original version; or |
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or |
||||||
|
authors of the material; or |
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some |
||||||
|
trade names, trademarks, or service marks; or |
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that |
||||||
|
material by anyone who conveys the material (or modified versions of |
||||||
|
it) with contractual assumptions of liability to the recipient, for |
||||||
|
any liability that these contractual assumptions directly impose on |
||||||
|
those licensors and authors. |
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further |
||||||
|
restrictions" within the meaning of section 10. If the Program as you |
||||||
|
received it, or any part of it, contains a notice stating that it is |
||||||
|
governed by this License along with a term that is a further |
||||||
|
restriction, you may remove that term. If a license document contains |
||||||
|
a further restriction but permits relicensing or conveying under this |
||||||
|
License, you may add to a covered work material governed by the terms |
||||||
|
of that license document, provided that the further restriction does |
||||||
|
not survive such relicensing or conveying. |
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you |
||||||
|
must place, in the relevant source files, a statement of the |
||||||
|
additional terms that apply to those files, or a notice indicating |
||||||
|
where to find the applicable terms. |
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the |
||||||
|
form of a separately written license, or stated as exceptions; |
||||||
|
the above requirements apply either way. |
||||||
|
|
||||||
|
8. Termination. |
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly |
||||||
|
provided under this License. Any attempt otherwise to propagate or |
||||||
|
modify it is void, and will automatically terminate your rights under |
||||||
|
this License (including any patent licenses granted under the third |
||||||
|
paragraph of section 11). |
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your |
||||||
|
license from a particular copyright holder is reinstated (a) |
||||||
|
provisionally, unless and until the copyright holder explicitly and |
||||||
|
finally terminates your license, and (b) permanently, if the copyright |
||||||
|
holder fails to notify you of the violation by some reasonable means |
||||||
|
prior to 60 days after the cessation. |
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is |
||||||
|
reinstated permanently if the copyright holder notifies you of the |
||||||
|
violation by some reasonable means, this is the first time you have |
||||||
|
received notice of violation of this License (for any work) from that |
||||||
|
copyright holder, and you cure the violation prior to 30 days after |
||||||
|
your receipt of the notice. |
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the |
||||||
|
licenses of parties who have received copies or rights from you under |
||||||
|
this License. If your rights have been terminated and not permanently |
||||||
|
reinstated, you do not qualify to receive new licenses for the same |
||||||
|
material under section 10. |
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies. |
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or |
||||||
|
run a copy of the Program. Ancillary propagation of a covered work |
||||||
|
occurring solely as a consequence of using peer-to-peer transmission |
||||||
|
to receive a copy likewise does not require acceptance. However, |
||||||
|
nothing other than this License grants you permission to propagate or |
||||||
|
modify any covered work. These actions infringe copyright if you do |
||||||
|
not accept this License. Therefore, by modifying or propagating a |
||||||
|
covered work, you indicate your acceptance of this License to do so. |
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients. |
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically |
||||||
|
receives a license from the original licensors, to run, modify and |
||||||
|
propagate that work, subject to this License. You are not responsible |
||||||
|
for enforcing compliance by third parties with this License. |
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an |
||||||
|
organization, or substantially all assets of one, or subdividing an |
||||||
|
organization, or merging organizations. If propagation of a covered |
||||||
|
work results from an entity transaction, each party to that |
||||||
|
transaction who receives a copy of the work also receives whatever |
||||||
|
licenses to the work the party's predecessor in interest had or could |
||||||
|
give under the previous paragraph, plus a right to possession of the |
||||||
|
Corresponding Source of the work from the predecessor in interest, if |
||||||
|
the predecessor has it or can get it with reasonable efforts. |
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the |
||||||
|
rights granted or affirmed under this License. For example, you may |
||||||
|
not impose a license fee, royalty, or other charge for exercise of |
||||||
|
rights granted under this License, and you may not initiate litigation |
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that |
||||||
|
any patent claim is infringed by making, using, selling, offering for |
||||||
|
sale, or importing the Program or any portion of it. |
||||||
|
|
||||||
|
11. Patents. |
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this |
||||||
|
License of the Program or a work on which the Program is based. The |
||||||
|
work thus licensed is called the contributor's "contributor version". |
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims |
||||||
|
owned or controlled by the contributor, whether already acquired or |
||||||
|
hereafter acquired, that would be infringed by some manner, permitted |
||||||
|
by this License, of making, using, or selling its contributor version, |
||||||
|
but do not include claims that would be infringed only as a |
||||||
|
consequence of further modification of the contributor version. For |
||||||
|
purposes of this definition, "control" includes the right to grant |
||||||
|
patent sublicenses in a manner consistent with the requirements of |
||||||
|
this License. |
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free |
||||||
|
patent license under the contributor's essential patent claims, to |
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and |
||||||
|
propagate the contents of its contributor version. |
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express |
||||||
|
agreement or commitment, however denominated, not to enforce a patent |
||||||
|
(such as an express permission to practice a patent or covenant not to |
||||||
|
sue for patent infringement). To "grant" such a patent license to a |
||||||
|
party means to make such an agreement or commitment not to enforce a |
||||||
|
patent against the party. |
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license, |
||||||
|
and the Corresponding Source of the work is not available for anyone |
||||||
|
to copy, free of charge and under the terms of this License, through a |
||||||
|
publicly available network server or other readily accessible means, |
||||||
|
then you must either (1) cause the Corresponding Source to be so |
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the |
||||||
|
patent license for this particular work, or (3) arrange, in a manner |
||||||
|
consistent with the requirements of this License, to extend the patent |
||||||
|
license to downstream recipients. "Knowingly relying" means you have |
||||||
|
actual knowledge that, but for the patent license, your conveying the |
||||||
|
covered work in a country, or your recipient's use of the covered work |
||||||
|
in a country, would infringe one or more identifiable patents in that |
||||||
|
country that you have reason to believe are valid. |
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or |
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a |
||||||
|
covered work, and grant a patent license to some of the parties |
||||||
|
receiving the covered work authorizing them to use, propagate, modify |
||||||
|
or convey a specific copy of the covered work, then the patent license |
||||||
|
you grant is automatically extended to all recipients of the covered |
||||||
|
work and works based on it. |
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within |
||||||
|
the scope of its coverage, prohibits the exercise of, or is |
||||||
|
conditioned on the non-exercise of one or more of the rights that are |
||||||
|
specifically granted under this License. You may not convey a covered |
||||||
|
work if you are a party to an arrangement with a third party that is |
||||||
|
in the business of distributing software, under which you make payment |
||||||
|
to the third party based on the extent of your activity of conveying |
||||||
|
the work, and under which the third party grants, to any of the |
||||||
|
parties who would receive the covered work from you, a discriminatory |
||||||
|
patent license (a) in connection with copies of the covered work |
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily |
||||||
|
for and in connection with specific products or compilations that |
||||||
|
contain the covered work, unless you entered into that arrangement, |
||||||
|
or that patent license was granted, prior to 28 March 2007. |
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting |
||||||
|
any implied license or other defenses to infringement that may |
||||||
|
otherwise be available to you under applicable patent law. |
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom. |
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or |
||||||
|
otherwise) that contradict the conditions of this License, they do not |
||||||
|
excuse you from the conditions of this License. If you cannot convey a |
||||||
|
covered work so as to satisfy simultaneously your obligations under this |
||||||
|
License and any other pertinent obligations, then as a consequence you may |
||||||
|
not convey it at all. For example, if you agree to terms that obligate you |
||||||
|
to collect a royalty for further conveying from those to whom you convey |
||||||
|
the Program, the only way you could satisfy both those terms and this |
||||||
|
License would be to refrain entirely from conveying the Program. |
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License. |
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have |
||||||
|
permission to link or combine any covered work with a work licensed |
||||||
|
under version 3 of the GNU Affero General Public License into a single |
||||||
|
combined work, and to convey the resulting work. The terms of this |
||||||
|
License will continue to apply to the part which is the covered work, |
||||||
|
but the special requirements of the GNU Affero General Public License, |
||||||
|
section 13, concerning interaction through a network will apply to the |
||||||
|
combination as such. |
||||||
|
|
||||||
|
14. Revised Versions of this License. |
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of |
||||||
|
the GNU General Public License from time to time. Such new versions will |
||||||
|
be similar in spirit to the present version, but may differ in detail to |
||||||
|
address new problems or concerns. |
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the |
||||||
|
Program specifies that a certain numbered version of the GNU General |
||||||
|
Public License "or any later version" applies to it, you have the |
||||||
|
option of following the terms and conditions either of that numbered |
||||||
|
version or of any later version published by the Free Software |
||||||
|
Foundation. If the Program does not specify a version number of the |
||||||
|
GNU General Public License, you may choose any version ever published |
||||||
|
by the Free Software Foundation. |
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future |
||||||
|
versions of the GNU General Public License can be used, that proxy's |
||||||
|
public statement of acceptance of a version permanently authorizes you |
||||||
|
to choose that version for the Program. |
||||||
|
|
||||||
|
Later license versions may give you additional or different |
||||||
|
permissions. However, no additional obligations are imposed on any |
||||||
|
author or copyright holder as a result of your choosing to follow a |
||||||
|
later version. |
||||||
|
|
||||||
|
15. Disclaimer of Warranty. |
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY |
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT |
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY |
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, |
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM |
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF |
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION. |
||||||
|
|
||||||
|
16. Limitation of Liability. |
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING |
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS |
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY |
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE |
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF |
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD |
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), |
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF |
||||||
|
SUCH DAMAGES. |
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16. |
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided |
||||||
|
above cannot be given local legal effect according to their terms, |
||||||
|
reviewing courts shall apply local law that most closely approximates |
||||||
|
an absolute waiver of all civil liability in connection with the |
||||||
|
Program, unless a warranty or assumption of liability accompanies a |
||||||
|
copy of the Program in return for a fee. |
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS |
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs |
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest |
||||||
|
possible use to the public, the best way to achieve this is to make it |
||||||
|
free software which everyone can redistribute and change under these terms. |
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest |
||||||
|
to attach them to the start of each source file to most effectively |
||||||
|
state the exclusion of warranty; and each file should have at least |
||||||
|
the "copyright" line and a pointer to where the full notice is found. |
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.> |
||||||
|
Copyright (C) <year> <name of author> |
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify |
||||||
|
it under the terms of the GNU General Public License as published by |
||||||
|
the Free Software Foundation, either version 3 of the License, or |
||||||
|
(at your option) any later version. |
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, |
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
GNU General Public License for more details. |
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License |
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail. |
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short |
||||||
|
notice like this when it starts in an interactive mode: |
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author> |
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. |
||||||
|
This is free software, and you are welcome to redistribute it |
||||||
|
under certain conditions; type `show c' for details. |
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate |
||||||
|
parts of the General Public License. Of course, your program's commands |
||||||
|
might be different; for a GUI interface, you would use an "about box". |
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school, |
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary. |
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see |
||||||
|
<https://www.gnu.org/licenses/>. |
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program |
||||||
|
into proprietary programs. If your program is a subroutine library, you |
||||||
|
may consider it more useful to permit linking proprietary applications with |
||||||
|
the library. If this is what you want to do, use the GNU Lesser General |
||||||
|
Public License instead of this License. But first, please read |
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>. |
||||||
@ -0,0 +1,430 @@ |
|||||||
|
[简体中文](README-CN.md) | [English](README.md) |
||||||
|
|
||||||
|
<div align="center"> |
||||||
|
|
||||||
|
# TypePHP |
||||||
|
|
||||||
|
**PHP 原生 AOT 编译器** |
||||||
|
|
||||||
|
将 PHP 源码提前(AOT)编译为原生机器码,生成独立的可执行文件、PHP 扩展和静态库, |
||||||
|
同时保留你熟悉的 PHP 语法。 |
||||||
|
|
||||||
|
</div> |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## 什么是 TypePHP? |
||||||
|
|
||||||
|
TypePHP 是一个 AOT(Ahead-Of-Time,提前编译)编译器,它把 PHP 源码翻译为 C++, |
||||||
|
再编译为原生机器码。与字节码缓存或虚拟机不同,它不会在运行时解释 opcode, |
||||||
|
而是直接生成在 CPU 上运行的原生二进制。 |
||||||
|
|
||||||
|
它保留熟悉的 PHP 语法,同时引入编译期类型信息,让编译器为你的性能热点生成快速、 |
||||||
|
静态类型的 C++ 代码,而其余代码仍运行在久经考验的 Zend 引擎上。 |
||||||
|
|
||||||
|
## 特性 |
||||||
|
|
||||||
|
- **真正的 AOT 编译** —— PHP 先降级为 C++17,再编译为原生机器码。无解释器、 |
||||||
|
无 opcode 缓存、无 JIT 预热。 |
||||||
|
- **三种构建模式** —— 同一份代码可编译为独立 `bin` 可执行文件、可加载的 PHP |
||||||
|
`ext` 扩展,或 `lib` 静态库。 |
||||||
|
- **原生类型系统** —— `int`、`float`、`bool` 直接映射为 C++ 标量类型 |
||||||
|
(`int64_t`、`double`、`bool`),数值代码可获得数量级的性能提升。 |
||||||
|
- **高精度数值** —— `bigInt`(GMP)、`decimal`(libmpdec)、`bigFloat`(MPFR), |
||||||
|
零开销算术运算。 |
||||||
|
- **强类型容器** —— `std::array`、`std::vector`、`std::map`、`std::ordered_map`, |
||||||
|
元素类型在编译期确定;最高比 PHP 数组快 **10 倍**,性能与 C++ `std::vector` 相当。 |
||||||
|
- **通用方法(Universal Methods)** —— 在原生类型上直接调用方法 |
||||||
|
(`$s->upper()`、`$arr->contains()`、`$big->mul(2)`),零运行时派发开销。 |
||||||
|
- **混合 C++ / PHP 编程** —— 在性能关键内核中直接调用 C++ 函数(反之亦然)。 |
||||||
|
- **编译期函数与关键词** —— `any()`、`refval()`、`objval()`、`expected()`、 |
||||||
|
`unexpected()`,以及 `toInt()`、`toString()`、`toArray()` 等。 |
||||||
|
- **编译期安全检查** —— `#[Immutable]` 只读契约和 `#[ArrayDef]` 数组结构元数据, |
||||||
|
在编译期检查,零运行时开销。 |
||||||
|
- **现代 PHP 支持** —— PHP 8.4 property hooks、非对称可见性、PHP 8.5 |
||||||
|
`clone()`-with 以及 `(void)` 丢弃表达式。 |
||||||
|
- **跨平台与 WASM** —— 面向 x86-64 和 ARM64 的 Linux、Windows、macOS 目标, |
||||||
|
以及 WASI 0.2 和浏览器(Jco)输出。 |
||||||
|
- **Python 桥接** —— 为 Python 模块生成 IDE helper,并将 Python 脚本转换为 TypePHP。 |
||||||
|
|
||||||
|
## 为什么选择 TypePHP? |
||||||
|
|
||||||
|
| | TypePHP AOT | 字节码缓存(OPcache) | JIT(PHP 8+) | |
||||||
|
|---|---|---|---| |
||||||
|
| 编译目标 | 原生机器码 | 字节码 | 机器码(trace) | |
||||||
|
| 启动 / 预热 | 无(已编译完成) | 每进程预热 | JIT 预热 | |
||||||
|
| 类型驱动优化 | 编译期、全程序 | 无 | 有限,基于 trace | |
||||||
|
| 独立可执行文件 | 支持 | 不支持 | 不支持 | |
||||||
|
| 源码保护 | 编译为机器码 | 字节码(可还原) | 字节码(可还原) | |
||||||
|
| 性能确定性 | 是 | 否 | 否 | |
||||||
|
|
||||||
|
**相较原生 PHP 的优势:** |
||||||
|
|
||||||
|
- **接近原生的性能。** 数值密集和容器密集的热点路径会编译为与 C++ 程序相同的机器码。 |
||||||
|
见下方[基准测试](#基准测试)。 |
||||||
|
- **源码保护。** 源码被编译掉——交付物是原生二进制,而不是可读的 PHP 文件。 |
||||||
|
- **零依赖部署。** 二进制模式生成单个自包含可执行文件,无需 PHP 运行时即可运行。 |
||||||
|
- **渐进式类型,真正带来收益。** 只在性能关键处添加 `use native_types`、`std::` |
||||||
|
容器和类型声明,其余保持普通 PHP。 |
||||||
|
- **完整 PHP 生态互通。** 扩展模式以标准 PHP 扩展形式加载到 `php-fpm`, |
||||||
|
现有框架和工具链可继续使用。 |
||||||
|
|
||||||
|
## 前置要求 |
||||||
|
|
||||||
|
- **PHP 8.4 – 8.5**,需包含 `embed` 模块(`libphp.so`) |
||||||
|
- **GCC 9+**(或 Clang),支持 **C++17** |
||||||
|
- **CMake 3.24+** |
||||||
|
- 高精度数学库:**GMP**、**MPFR**、**libmpdec** |
||||||
|
|
||||||
|
```shell |
||||||
|
# Ubuntu/Debian |
||||||
|
sudo apt install libgmp-dev libmpfr-dev libmpdec-dev |
||||||
|
|
||||||
|
# RHEL/CentOS/Fedora |
||||||
|
sudo dnf install gmp-devel mpfr-devel libmpdec-devel |
||||||
|
|
||||||
|
# Arch Linux |
||||||
|
sudo pacman -S gmp mpfr mpdecimal |
||||||
|
``` |
||||||
|
|
||||||
|
> GMP 用于 `bigInt`,MPFR 用于 `bigFloat`,libmpdec 用于 `decimal`。 |
||||||
|
|
||||||
|
预览版目前以 **Linux** 为主要开发平台(推荐 Ubuntu 22.04)。Windows 和 macOS |
||||||
|
打包通过同一入口点支持。 |
||||||
|
|
||||||
|
## 安装 |
||||||
|
|
||||||
|
### 通过 Composer |
||||||
|
|
||||||
|
```bash |
||||||
|
composer require --dev swoole/typephp |
||||||
|
``` |
||||||
|
|
||||||
|
然后编译你的项目: |
||||||
|
|
||||||
|
```bash |
||||||
|
vendor/bin/tpc.php project.yml |
||||||
|
``` |
||||||
|
|
||||||
|
在 TypePHP 源码仓库中开发时,改用本地入口: |
||||||
|
|
||||||
|
```bash |
||||||
|
bin/tpc.php project.yml |
||||||
|
``` |
||||||
|
|
||||||
|
### 构建 `libphp.so` |
||||||
|
|
||||||
|
`tpc` 需要以 `embed` SAPI 构建的 PHP。如果 Linux 上缺少 `libphp.so`, |
||||||
|
`tpc.php` 可以交互式下载 PHP 源码并自动构建。详见 |
||||||
|
[自动构建 libphp.so](docs/LIBPHP_INSTALLER.md)。 |
||||||
|
|
||||||
|
## 快速开始 |
||||||
|
|
||||||
|
创建 `hello.php`: |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
echo "Hello World!\n"; |
||||||
|
var_dump(PHP_VERSION); |
||||||
|
var_dump(php_uname()); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
编译并运行: |
||||||
|
|
||||||
|
```bash |
||||||
|
bin/tpc.php hello.php |
||||||
|
./hello |
||||||
|
``` |
||||||
|
|
||||||
|
输出: |
||||||
|
|
||||||
|
``` |
||||||
|
Hello World! |
||||||
|
string(5) "8.4.x" |
||||||
|
string(16) "Linux ..." |
||||||
|
``` |
||||||
|
|
||||||
|
> 二进制模式需要全局 `main()` 函数。它可以声明为无参数,或 |
||||||
|
> `main(int $argc, array $argv)` 以接收命令行参数,且必须返回 `void`。 |
||||||
|
|
||||||
|
## 编译模式 |
||||||
|
|
||||||
|
TypePHP 支持三种构建模式,通过 `-m` / `--mode` 选择: |
||||||
|
|
||||||
|
| 模式 | 参数 | 输出 | 需要 `main()` | 典型用途 | |
||||||
|
|---|---|---|---|---| |
||||||
|
| 二进制 | `-m bin`(默认) | 可执行文件 | 是 | CLI 工具、常驻服务、独立应用 | |
||||||
|
| 扩展 | `-m ext` | `.so` / `.dll` | 否 | `php-fpm` 上的 Web 应用、即插即用 PHP 扩展 | |
||||||
|
| 库 | `-m lib` | 静态库 | 否 | 将编译后的代码嵌入其他项目 | |
||||||
|
|
||||||
|
```bash |
||||||
|
# 二进制(默认) |
||||||
|
bin/tpc.php app.php -o myapp |
||||||
|
|
||||||
|
# PHP 扩展 |
||||||
|
bin/tpc.php extension/ -m ext -o my_extension |
||||||
|
|
||||||
|
# 静态库 |
||||||
|
bin/tpc.php lib/ -m lib -o mylib |
||||||
|
``` |
||||||
|
|
||||||
|
详见[编译模式](docs/COMPILATION_MODES.md)。 |
||||||
|
|
||||||
|
## 使用示例 |
||||||
|
|
||||||
|
### 1. 原生类型 —— 编译期数值加速 |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
use native_types; |
||||||
|
|
||||||
|
function fib(int $n): int |
||||||
|
{ |
||||||
|
if ($n == 1 || $n == 2) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
return fib($n - 1) + fib($n - 2); |
||||||
|
} |
||||||
|
|
||||||
|
function main(int $argc, array $argv): void |
||||||
|
{ |
||||||
|
$n = (int)$argv[1]; |
||||||
|
$begin = microtime(true); |
||||||
|
echo fib($n) . "\n"; |
||||||
|
echo "Time: " . (microtime(true) - $begin) . "\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```bash |
||||||
|
bin/tpc.php fib.php -O3 -o fib |
||||||
|
./fib 30 |
||||||
|
``` |
||||||
|
|
||||||
|
使用 `use native_types` 后,`int` 变量变为 C++ `int64_t`,算术运算直接编译为 |
||||||
|
CPU 指令,而不是 ZendVM 调用。 |
||||||
|
|
||||||
|
### 2. 高精度数值 |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
declare(strict_types=1); |
||||||
|
use native_types; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
// 54 位整数 —— 自动识别并存储为 bigInt |
||||||
|
$a = std::bigInt("123456789012345678901234567890123456789012345678901234"); |
||||||
|
$b = std::bigInt("987654321098765432109876543210987654321098765432109876"); |
||||||
|
|
||||||
|
echo $a->add($b)->toString() . "\n"; // 精确计算,不会溢出 |
||||||
|
|
||||||
|
// 精确的十进制运算 —— 无二进制浮点误差 |
||||||
|
$c = std::decimal("0.1")->add(std::decimal("0.2")); |
||||||
|
echo $c->toString() . "\n"; // "0.3" |
||||||
|
|
||||||
|
// 256 位浮点数 |
||||||
|
$pi = std::bigFloat("3.14159265358979323846264338327950288419716939937510"); |
||||||
|
echo $pi->mul(2)->toString() . "\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
详见[高精度类型](docs/HIGH_PRECISION_TYPES.md)和[原生类型](docs/NATIVE_TYPES.md)。 |
||||||
|
|
||||||
|
### 3. 强类型容器 |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
use native_types; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$vector = std::vector(Type::Int); |
||||||
|
|
||||||
|
$vector[] = 1; |
||||||
|
$vector[] = 2; |
||||||
|
$vector[] = 3; |
||||||
|
|
||||||
|
$sum = 0; |
||||||
|
foreach ($vector as $value) { |
||||||
|
$sum += $value; |
||||||
|
} |
||||||
|
|
||||||
|
echo $sum . "\n"; // 6 |
||||||
|
echo $vector[1] . "\n"; // 2 |
||||||
|
|
||||||
|
// 固定 key/value 类型的映射 |
||||||
|
$map = std::ordered_map(Type::String, Type::Int); |
||||||
|
$map["a"] = 1; |
||||||
|
$map["b"] = 2; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
详见 [Std 容器](docs/STD_CONTAINERS.md)。 |
||||||
|
|
||||||
|
### 4. 通用方法 |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$s = "hello world"; |
||||||
|
echo $s->length() . "\n"; // strlen() |
||||||
|
echo $s->upper() . "\n"; // strtoupper() |
||||||
|
echo $s->substr(0, 5) . "\n"; // substr() |
||||||
|
|
||||||
|
$arr = [1, 3, 5, 7, 9]; |
||||||
|
echo $arr->count() . "\n"; // count() |
||||||
|
var_dump($arr->contains(3)); // in_array() |
||||||
|
|
||||||
|
$big = std::bigInt("12345678901234567890"); |
||||||
|
echo $big->mul(2)->toString() . "\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
原生类型上的方法调用在编译期被解析为直接的 C/C++ 函数调用——没有虚表查找、 |
||||||
|
没有反射、没有运行时派发。详见[通用方法](docs/UNIVERSAL_METHODS.md)。 |
||||||
|
|
||||||
|
### 5. 混合 C++ / PHP |
||||||
|
|
||||||
|
用 C++ 编写性能关键内核,并在 PHP 中调用: |
||||||
|
|
||||||
|
```cpp |
||||||
|
// math.cpp |
||||||
|
#include <phpx.h> |
||||||
|
|
||||||
|
using namespace php; |
||||||
|
|
||||||
|
var php_fast_sum(Int a, Int b) { |
||||||
|
return a + b; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
// math.stub.php —— 声明 C++ 函数签名 |
||||||
|
function fast_sum(int $a, int $b): int; |
||||||
|
``` |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
echo fast_sum(3, 4) . "\n"; // 7 |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
详见[混合 C++/PHP](docs/MIXED_CPP_PHP.md)。 |
||||||
|
|
||||||
|
## 基准测试 |
||||||
|
|
||||||
|
一个 10000×100000 的元素累加循环,对比 PHP 数组、TypePHP `std::array` |
||||||
|
与原生 C++: |
||||||
|
|
||||||
|
| 实现 | 耗时 | |
||||||
|
|---|---| |
||||||
|
| PHP 数组(JIT) | 67.6 秒 | |
||||||
|
| `std::array`(TypePHP AOT) | **6.4 秒** | |
||||||
|
| C++ `std::vector` | 6.2 秒 | |
||||||
|
|
||||||
|
`std::array` 比 PHP 数组快约 **10 倍**,性能与手写 C++ 完全一致。 |
||||||
|
完整基准测试见 [Std 容器](docs/STD_CONTAINERS.md)。 |
||||||
|
|
||||||
|
## 命令行 |
||||||
|
|
||||||
|
```bash |
||||||
|
bin/tpc.php <file|dir|project.yml> [options] [-- program-args...] |
||||||
|
``` |
||||||
|
|
||||||
|
常用示例: |
||||||
|
|
||||||
|
```bash |
||||||
|
# 编译单个文件 |
||||||
|
bin/tpc.php app.php |
||||||
|
|
||||||
|
# 优化并运行,`--` 后的参数传给生成的程序 |
||||||
|
bin/tpc.php app.php -O3 -r -- --flag value |
||||||
|
|
||||||
|
# 编译 project.yml 定义的项目 |
||||||
|
bin/tpc.php project.yml -O2 -j 8 |
||||||
|
|
||||||
|
# 生成 PHP 扩展 |
||||||
|
bin/tpc.php extension/ -m ext -o my_extension |
||||||
|
|
||||||
|
# 只生成 C++(跳过编译与链接) |
||||||
|
bin/tpc.php app.php --dry --build-dir /tmp/typephp-build |
||||||
|
|
||||||
|
# 编译为 WASI 0.2 |
||||||
|
bin/tpc.php --wasm app.php |
||||||
|
|
||||||
|
# 编译为浏览器目标(需要 jco) |
||||||
|
bin/tpc.php --wasm=browser app.php |
||||||
|
``` |
||||||
|
|
||||||
|
主要选项: |
||||||
|
|
||||||
|
| 选项 | 说明 | |
||||||
|
|---|---| |
||||||
|
| `-O <0-3>` | 优化级别(默认 `0`) | |
||||||
|
| `-d`, `--debug` | 调试构建,带符号和源码跟踪 | |
||||||
|
| `-o`, `--output <file>` | 输出文件名 | |
||||||
|
| `-m`, `--mode <bin\|lib\|ext>` | 构建模式(默认 `bin`) | |
||||||
|
| `-r`, `--run` | 构建成功后运行 | |
||||||
|
| `-j`, `--job <num>` | 并行编译任务数(默认 `4`) | |
||||||
|
| `--build-dir <dir>` | 生成 C++ 与中间产物的目录 | |
||||||
|
| `--dry` | 只生成 C++,跳过编译与链接 | |
||||||
|
| `--php-version <8.4\|8.5>` | 接受的 PHP 语法版本 | |
||||||
|
| `--cxx-std <ver>` | C++ 标准(如 `c++17`、`c++20`) | |
||||||
|
| `--march <arch>` | 目标指令集(如 `native`) | |
||||||
|
| `--lto` | 启用链接时优化 | |
||||||
|
| `--sanitize <type>` | 启用 sanitizer(如 `address`) | |
||||||
|
|
||||||
|
运行 `bin/tpc.php --help` 查看权威的最新参数列表。详见 |
||||||
|
[编译器命令行](docs/COMPILER_CLI.md),包括 Bash 补全: |
||||||
|
|
||||||
|
```bash |
||||||
|
source <(./tpc --generate-completion=bash) |
||||||
|
``` |
||||||
|
|
||||||
|
## Python 桥接 |
||||||
|
|
||||||
|
TypePHP 内置一个 Python 工具子模块,复用 `tpc` 入口: |
||||||
|
|
||||||
|
```shell |
||||||
|
# 为 Python 模块生成 IDE helper |
||||||
|
./tpc --gen-python-helper math |
||||||
|
./tpc --gen-python-helper numpy --output-dir .ide-helper |
||||||
|
|
||||||
|
# 将 Python 脚本转换为 TypePHP |
||||||
|
./tpc --convert-python-to-php script.py > script.php |
||||||
|
``` |
||||||
|
|
||||||
|
详见 [Python 工具子模块](docs/python/tools.md)。 |
||||||
|
|
||||||
|
## 文档 |
||||||
|
|
||||||
|
- [快速入门](docs/QUICKSTART.md) —— 最小编译流程 |
||||||
|
- [编译模式](docs/COMPILATION_MODES.md) —— `bin`、`ext`、`lib` |
||||||
|
- [编译器命令行](docs/COMPILER_CLI.md) —— CLI 参数与项目配置 |
||||||
|
- [不兼容 PHP 特性清单](docs/INCOMPATIBLE_PHP_FEATURES.md) —— 当前限制 |
||||||
|
- [原生类型](docs/NATIVE_TYPES.md) —— 原生标量类型 |
||||||
|
- [高精度类型](docs/HIGH_PRECISION_TYPES.md) —— BigInt / Decimal / BigFloat |
||||||
|
- [Std 容器](docs/STD_CONTAINERS.md) —— 强类型容器 |
||||||
|
- [通用方法](docs/UNIVERSAL_METHODS.md) —— 零开销方法 |
||||||
|
- [编译期函数](docs/COMPILE_TIME_FUNCTIONS.md) —— `any()`、`refval()`、`objval()` 等 |
||||||
|
- [混合 C++/PHP](docs/MIXED_CPP_PHP.md) —— C++/PHP 互操作 |
||||||
|
- [`#[Immutable]`](docs/IMMUTABLE.md) —— 编译期只读契约 |
||||||
|
- [WASI 构建](docs/WASI_BUILD.md) —— WASI 目标 |
||||||
|
|
||||||
|
## 授权协议 |
||||||
|
|
||||||
|
TypePHP 采用 [GNU General Public License v3.0](LICENSE) 授权。 |
||||||
|
|
||||||
|
## 社区 |
||||||
|
|
||||||
|
- 代码仓库:<https://github.com/swoole/typephp> |
||||||
|
- 版权所有 © 2026 上海识沃网络科技有限公司(Swoole) |
||||||
@ -1,84 +1,449 @@ |
|||||||
# 依赖 |
[English](README.md) | [简体中文](README-CN.md) |
||||||
- 编译器需要 PHP 8.4 以上版本;生成的扩展仍可面向 PHP 8.2~8.5 |
|
||||||
- 需要 GCC-9 以上版本,支持 C++17 标准 |
|
||||||
- 需要 CMake-3.24 以上版本 |
|
||||||
- 需要高精度数学库:`GMP`、`MPFR`、`libmpdec` |
|
||||||
|
|
||||||
# Composer 安装 |
<div align="center"> |
||||||
|
|
||||||
在项目中安装 TypePHP: |
# TypePHP |
||||||
|
|
||||||
|
**A native AOT compiler for PHP** |
||||||
|
|
||||||
|
Compile PHP source code into native machine code ahead of time — producing |
||||||
|
standalone executables, PHP extensions, and static libraries — while keeping |
||||||
|
the PHP syntax you already know. |
||||||
|
|
||||||
|
</div> |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## What is TypePHP? |
||||||
|
|
||||||
|
TypePHP is an Ahead-Of-Time (AOT) compiler that translates PHP source code into |
||||||
|
C++ and then into native machine code. Unlike a bytecode cache or a VM, it does |
||||||
|
not interpret opcodes at runtime: it generates optimized native binaries that |
||||||
|
run directly on the CPU. |
||||||
|
|
||||||
|
It keeps familiar PHP syntax and adds compile-time type information, so the |
||||||
|
compiler can emit fast, statically-typed C++ for your hot paths — while the |
||||||
|
rest of your code continues to run on the battle-tested Zend engine. |
||||||
|
|
||||||
|
## Features |
||||||
|
|
||||||
|
- **True AOT compilation** — PHP is lowered to C++17, then to native machine |
||||||
|
code. No interpreter, no opcode cache, no JIT warm-up. |
||||||
|
- **Three build modes** — build a standalone `bin` executable, a loadable PHP |
||||||
|
`ext` extension, or a `lib` static library from the same codebase. |
||||||
|
- **Native type system** — `int`, `float`, and `bool` map directly to C++ |
||||||
|
scalar types (`int64_t`, `double`, `bool`) for orders-of-magnitude speedups |
||||||
|
on numeric code. |
||||||
|
- **High-precision numerics** — `bigInt` (GMP), `decimal` (libmpdec), and |
||||||
|
`bigFloat` (MPFR) with zero-overhead arithmetic. |
||||||
|
- **Strongly-typed containers** — `std::array`, `std::vector`, `std::map`, and |
||||||
|
`std::ordered_map` with compile-time element types; up to **10×** faster than |
||||||
|
PHP arrays and on par with C++ `std::vector`. |
||||||
|
- **Universal methods** — call methods directly on primitives |
||||||
|
(`$s->upper()`, `$arr->contains()`, `$big->mul(2)`) with zero runtime |
||||||
|
dispatch overhead. |
||||||
|
- **Mixed C++ / PHP** — call C++ functions from PHP (and vice versa) for |
||||||
|
performance-critical kernels. |
||||||
|
- **Compile-time functions & keywords** — `any()`, `refval()`, `objval()`, |
||||||
|
`expected()`, `unexpected()`, plus `toInt()`, `toString()`, `toArray()` and |
||||||
|
friends. |
||||||
|
- **Compile-time safety** — `#[Immutable]` read-only contracts and `#[ArrayDef]` |
||||||
|
array-shape metadata, checked at compile time with zero runtime cost. |
||||||
|
- **Modern PHP support** — PHP 8.4 property hooks, asymmetric visibility, |
||||||
|
PHP 8.5 `clone()`-with, and `(void)` discard expressions. |
||||||
|
- **Cross-platform & WASM** — Linux, Windows, and macOS targets for x86-64 and |
||||||
|
ARM64, plus WASI 0.2 and browser (Jco) output. |
||||||
|
- **Python bridge** — generate IDE helpers for Python modules and convert |
||||||
|
Python scripts to TypePHP. |
||||||
|
|
||||||
|
## Why TypePHP? |
||||||
|
|
||||||
|
| | TypePHP AOT | Opcode cache (OPcache) | JIT (PHP 8+) | |
||||||
|
|---|---|---|---| |
||||||
|
| Compilation target | Native machine code | Bytecode | Machine code (trace) | |
||||||
|
| Startup / warm-up | None (already compiled) | Per-process warm-up | JIT warm-up | |
||||||
|
| Type-driven optimization | Compile-time, full-program | None | Limited, trace-based | |
||||||
|
| Standalone executable | Yes | No | No | |
||||||
|
| Source code protection | Compiled to machine code | Bytecode (reversible) | Bytecode (reversible) | |
||||||
|
| Deterministic performance | Yes | No | No | |
||||||
|
|
||||||
|
**Strengths over plain PHP:** |
||||||
|
|
||||||
|
- **Near-native performance.** Numeric and container-heavy hot paths compile |
||||||
|
down to the same machine code a C++ program would produce. See the |
||||||
|
[benchmark](#benchmark) below. |
||||||
|
- **Source protection.** Your source is compiled away — shipped artifacts are |
||||||
|
native binaries, not readable PHP files. |
||||||
|
- **Zero-dependency deployment.** Binary mode produces a single self-contained |
||||||
|
executable that runs without a PHP runtime. |
||||||
|
- **Gradual typing that actually pays off.** Add `use native_types`, `std::` |
||||||
|
containers, and type declarations only where performance matters; the rest |
||||||
|
stays ordinary PHP. |
||||||
|
- **Full PHP ecosystem interop.** Extension mode loads as a standard PHP |
||||||
|
extension into `php-fpm`, so existing frameworks and tooling keep working. |
||||||
|
|
||||||
|
## Requirements |
||||||
|
|
||||||
|
- **PHP 8.4 – 8.5** with the `embed` module (`libphp.so`) |
||||||
|
- **GCC 9+** (or Clang) with **C++17** |
||||||
|
- **CMake 3.24+** |
||||||
|
- High-precision math libraries: **GMP**, **MPFR**, **libmpdec** |
||||||
|
|
||||||
|
```shell |
||||||
|
# Ubuntu/Debian |
||||||
|
sudo apt install libgmp-dev libmpfr-dev libmpdec-dev |
||||||
|
|
||||||
|
# RHEL/CentOS/Fedora |
||||||
|
sudo dnf install gmp-devel mpfr-devel libmpdec-devel |
||||||
|
|
||||||
|
# Arch Linux |
||||||
|
sudo pacman -S gmp mpfr mpdecimal |
||||||
|
``` |
||||||
|
|
||||||
|
> GMP powers `bigInt`, MPFR powers `bigFloat`, and libmpdec powers `decimal`. |
||||||
|
|
||||||
|
The preview currently targets **Linux** as the primary development platform |
||||||
|
(Ubuntu 22.04 recommended). Windows and macOS packaging is supported through |
||||||
|
the same entry point. |
||||||
|
|
||||||
|
## Installation |
||||||
|
|
||||||
|
### Via Composer |
||||||
|
|
||||||
```bash |
```bash |
||||||
composer require --dev swoole/typephp |
composer require --dev swoole/typephp |
||||||
``` |
``` |
||||||
|
|
||||||
安装后可直接编译项目: |
Then compile your project: |
||||||
|
|
||||||
```bash |
```bash |
||||||
vendor/bin/tpc.php project.yml |
vendor/bin/tpc.php project.yml |
||||||
``` |
``` |
||||||
|
|
||||||
在 TypePHP 源码仓库中则使用: |
When working inside the TypePHP source repository, use the local entry point |
||||||
|
instead: |
||||||
|
|
||||||
```bash |
```bash |
||||||
bin/tpc.php project.yml |
bin/tpc.php project.yml |
||||||
``` |
``` |
||||||
|
|
||||||
Linux 环境缺少 `libphp.so` 时,`tpc.php` 可以交互式下载 PHP 源码并自动构建,详见 [自动构建 libphp.so](docs/LIBPHP_INSTALLER.md)。 |
### Building `libphp.so` |
||||||
|
|
||||||
```shell |
`tpc` requires a PHP built with the `embed` SAPI. If `libphp.so` is missing on |
||||||
# Ubuntu/Debian |
Linux, `tpc.php` can interactively download the PHP source and build it for |
||||||
sudo apt install libgmp-dev libmpfr-dev libmpdec-dev |
you. See [Automatic libphp.so build](docs/LIBPHP_INSTALLER.md). |
||||||
|
|
||||||
# RHEL/CentOS/Fedora |
## Quick Start |
||||||
sudo dnf install gmp-devel mpfr-devel libmpdec-devel |
|
||||||
|
|
||||||
# Arch Linux |
Create `hello.php`: |
||||||
sudo pacman -S gmp mpfr mpdecimal |
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
echo "Hello World!\n"; |
||||||
|
var_dump(PHP_VERSION); |
||||||
|
var_dump(php_uname()); |
||||||
|
} |
||||||
``` |
``` |
||||||
|
|
||||||
> GMP 用于 `BigInt` 任意精度整数,MPFR 用于 `BigFloat` 高精度浮点数,libmpdec 用于 `Decimal` 十进制高精度小数。 |
Compile and run it: |
||||||
|
|
||||||
> 预览版目前仅支持 `Linux` 系统,建议使用 `Ubuntu 22.04` |
```bash |
||||||
|
bin/tpc.php hello.php |
||||||
|
./hello |
||||||
|
``` |
||||||
|
|
||||||
|
Output: |
||||||
|
|
||||||
|
``` |
||||||
|
Hello World! |
||||||
|
string(5) "8.4.x" |
||||||
|
string(16) "Linux ..." |
||||||
|
``` |
||||||
|
|
||||||
## PHP |
> Binary mode requires a global `main()` function. It may be declared with no |
||||||
必须包含 embed 模块 |
> parameters, or as `main(int $argc, array $argv)` to receive command-line |
||||||
|
> arguments, and must return `void`. |
||||||
|
|
||||||
## PHPX |
## Compilation Modes |
||||||
可使用 `composer install` 安装依赖。 |
|
||||||
进入 `vendor/swoole/phpx` 目录,编译 `phpx` |
|
||||||
|
|
||||||
```shell |
TypePHP supports three build modes, selected with `-m` / `--mode`: |
||||||
cd vendor/swoole/phpx |
|
||||||
cmake . |
| Mode | Flag | Output | Needs `main()` | Typical use | |
||||||
make -j32 |
|---|---|---|---|---| |
||||||
|
| Binary | `-m bin` (default) | Executable | Yes | CLI tools, long-running services, standalone apps | |
||||||
|
| Extension | `-m ext` | `.so` / `.dll` | No | Web apps on `php-fpm`, drop-in PHP extension | |
||||||
|
| Library | `-m lib` | Static library | No | Embedding compiled code into other projects | |
||||||
|
|
||||||
|
```bash |
||||||
|
# Binary (default) |
||||||
|
bin/tpc.php app.php -o myapp |
||||||
|
|
||||||
|
# PHP extension |
||||||
|
bin/tpc.php extension/ -m ext -o my_extension |
||||||
|
|
||||||
|
# Static library |
||||||
|
bin/tpc.php lib/ -m lib -o mylib |
||||||
``` |
``` |
||||||
|
|
||||||
## 动态链接库 |
See [Compilation modes](docs/COMPILATION_MODES.md) for details. |
||||||
```shell |
|
||||||
sudo ldconfig -p | grep php |
## Examples |
||||||
|
|
||||||
|
### 1. Native types — compile-time numeric speedup |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
use native_types; |
||||||
|
|
||||||
|
function fib(int $n): int |
||||||
|
{ |
||||||
|
if ($n == 1 || $n == 2) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
return fib($n - 1) + fib($n - 2); |
||||||
|
} |
||||||
|
|
||||||
|
function main(int $argc, array $argv): void |
||||||
|
{ |
||||||
|
$n = (int)$argv[1]; |
||||||
|
$begin = microtime(true); |
||||||
|
echo fib($n) . "\n"; |
||||||
|
echo "Time: " . (microtime(true) - $begin) . "\n"; |
||||||
|
} |
||||||
``` |
``` |
||||||
必须包含 `libphp.so` 和 `libphpx.so` |
|
||||||
|
|
||||||
若编译完成,但找不到动态链接库,需要修改 |
```bash |
||||||
```shell |
bin/tpc.php fib.php -O3 -o fib |
||||||
vim /etc/ld.so.conf.d/swoole.conf |
./fib 30 |
||||||
``` |
``` |
||||||
|
|
||||||
添加路径 |
With `use native_types`, `int` variables become C++ `int64_t` and arithmetic |
||||||
|
compiles to plain CPU instructions instead of ZendVM calls. |
||||||
|
|
||||||
|
### 2. High-precision numerics |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
declare(strict_types=1); |
||||||
|
use native_types; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
// 54-digit integer — automatically detected and stored as bigInt |
||||||
|
$a = std::bigInt("123456789012345678901234567890123456789012345678901234"); |
||||||
|
$b = std::bigInt("987654321098765432109876543210987654321098765432109876"); |
||||||
|
|
||||||
|
echo $a->add($b)->toString() . "\n"; // exact, no overflow |
||||||
|
|
||||||
|
// Exact decimal arithmetic — no binary floating-point error |
||||||
|
$c = std::decimal("0.1")->add(std::decimal("0.2")); |
||||||
|
echo $c->toString() . "\n"; // "0.3" |
||||||
|
|
||||||
|
// 256-bit floating point |
||||||
|
$pi = std::bigFloat("3.14159265358979323846264338327950288419716939937510"); |
||||||
|
echo $pi->mul(2)->toString() . "\n"; |
||||||
|
} |
||||||
``` |
``` |
||||||
/home/swoole/workspace/projects/phpx/lib |
|
||||||
/opt/php-8.4/lib/ |
See [High-precision types](docs/HIGH_PRECISION_TYPES.md) and |
||||||
|
[Native types](docs/NATIVE_TYPES.md). |
||||||
|
|
||||||
|
### 3. Strongly-typed containers |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
use native_types; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$vector = std::vector(Type::Int); |
||||||
|
|
||||||
|
$vector[] = 1; |
||||||
|
$vector[] = 2; |
||||||
|
$vector[] = 3; |
||||||
|
|
||||||
|
$sum = 0; |
||||||
|
foreach ($vector as $value) { |
||||||
|
$sum += $value; |
||||||
|
} |
||||||
|
|
||||||
|
echo $sum . "\n"; // 6 |
||||||
|
echo $vector[1] . "\n"; // 2 |
||||||
|
|
||||||
|
// key-value map with fixed key/value types |
||||||
|
$map = std::ordered_map(Type::String, Type::Int); |
||||||
|
$map["a"] = 1; |
||||||
|
$map["b"] = 2; |
||||||
|
} |
||||||
``` |
``` |
||||||
|
|
||||||
## Release packaging |
See [Std containers](docs/STD_CONTAINERS.md). |
||||||
|
|
||||||
|
### 4. Universal methods |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$s = "hello world"; |
||||||
|
echo $s->length() . "\n"; // strlen() |
||||||
|
echo $s->upper() . "\n"; // strtoupper() |
||||||
|
echo $s->substr(0, 5) . "\n"; // substr() |
||||||
|
|
||||||
|
$arr = [1, 3, 5, 7, 9]; |
||||||
|
echo $arr->count() . "\n"; // count() |
||||||
|
var_dump($arr->contains(3)); // in_array() |
||||||
|
|
||||||
|
$big = std::bigInt("12345678901234567890"); |
||||||
|
echo $big->mul(2)->toString() . "\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Method calls on primitives are resolved at compile time into direct C/C++ |
||||||
|
function calls — no vtable lookup, no reflection, no runtime dispatch. See |
||||||
|
[Universal methods](docs/UNIVERSAL_METHODS.md). |
||||||
|
|
||||||
|
### 5. Mixed C++ / PHP |
||||||
|
|
||||||
|
Write performance-critical kernels in C++ and call them from PHP: |
||||||
|
|
||||||
|
```cpp |
||||||
|
// math.cpp |
||||||
|
#include <phpx.h> |
||||||
|
|
||||||
|
using namespace php; |
||||||
|
|
||||||
|
var php_fast_sum(Int a, Int b) { |
||||||
|
return a + b; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
// math.stub.php — declares the C++ function signature |
||||||
|
function fast_sum(int $a, int $b): int; |
||||||
|
``` |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
echo fast_sum(3, 4) . "\n"; // 7 |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
See [Mixed C++/PHP](docs/MIXED_CPP_PHP.md). |
||||||
|
|
||||||
|
## Benchmark |
||||||
|
|
||||||
|
A 10000×100000 element update loop, comparing PHP arrays against TypePHP's |
||||||
|
`std::array` and native C++: |
||||||
|
|
||||||
Use the same PHP entry point on Windows, Linux, and macOS: |
| Implementation | Time | |
||||||
|
|---|---| |
||||||
|
| PHP array (JIT) | 67.6 s | |
||||||
|
| `std::array` (TypePHP AOT) | **6.4 s** | |
||||||
|
| C++ `std::vector` | 6.2 s | |
||||||
|
|
||||||
|
`std::array` is roughly **10× faster** than PHP arrays and performs |
||||||
|
identically to hand-written C++. See the full benchmark in |
||||||
|
[Std containers](docs/STD_CONTAINERS.md). |
||||||
|
|
||||||
|
## Command Line |
||||||
|
|
||||||
|
```bash |
||||||
|
bin/tpc.php <file|dir|project.yml> [options] [-- program-args...] |
||||||
|
``` |
||||||
|
|
||||||
|
Common usage: |
||||||
|
|
||||||
|
```bash |
||||||
|
# Compile a single file |
||||||
|
bin/tpc.php app.php |
||||||
|
|
||||||
|
# Optimize and run, passing args to the program after `--` |
||||||
|
bin/tpc.php app.php -O3 -r -- --flag value |
||||||
|
|
||||||
|
# Compile a project defined in project.yml |
||||||
|
bin/tpc.php project.yml -O2 -j 8 |
||||||
|
|
||||||
|
# Build a PHP extension |
||||||
|
bin/tpc.php extension/ -m ext -o my_extension |
||||||
|
|
||||||
|
# Only generate C++ (skip compile & link) |
||||||
|
bin/tpc.php app.php --dry --build-dir /tmp/typephp-build |
||||||
|
|
||||||
|
# Compile to WASI 0.2 |
||||||
|
bin/tpc.php --wasm app.php |
||||||
|
|
||||||
|
# Compile for the browser (requires jco) |
||||||
|
bin/tpc.php --wasm=browser app.php |
||||||
|
``` |
||||||
|
|
||||||
|
Key options: |
||||||
|
|
||||||
|
| Option | Description | |
||||||
|
|---|---| |
||||||
|
| `-O <0-3>` | Optimization level (default `0`) | |
||||||
|
| `-d`, `--debug` | Debug build with symbols and source tracking | |
||||||
|
| `-o`, `--output <file>` | Output file name | |
||||||
|
| `-m`, `--mode <bin\|lib\|ext>` | Build mode (default `bin`) | |
||||||
|
| `-r`, `--run` | Run after a successful build | |
||||||
|
| `-j`, `--job <num>` | Parallel compile jobs (default `4`) | |
||||||
|
| `--build-dir <dir>` | Directory for generated C++ and intermediates | |
||||||
|
| `--dry` | Generate C++ only, skip compile and link | |
||||||
|
| `--php-version <8.4\|8.5>` | PHP syntax version to accept | |
||||||
|
| `--cxx-std <ver>` | C++ standard (e.g. `c++17`, `c++20`) | |
||||||
|
| `--march <arch>` | Target instruction set (e.g. `native`) | |
||||||
|
| `--lto` | Enable link-time optimization | |
||||||
|
| `--sanitize <type>` | Enable a sanitizer (e.g. `address`) | |
||||||
|
|
||||||
|
Run `bin/tpc.php --help` for the authoritative, up-to-date list. See |
||||||
|
[Compiler CLI](docs/COMPILER_CLI.md) for details, including Bash completion: |
||||||
|
|
||||||
|
```bash |
||||||
|
source <(./tpc --generate-completion=bash) |
||||||
|
``` |
||||||
|
|
||||||
|
## Python bridge |
||||||
|
|
||||||
|
TypePHP ships a Python tool submodule that shares the `tpc` entry point: |
||||||
|
|
||||||
```shell |
```shell |
||||||
php package.php |
# Generate IDE helpers for Python modules |
||||||
|
./tpc --gen-python-helper math |
||||||
|
./tpc --gen-python-helper numpy --output-dir .ide-helper |
||||||
|
|
||||||
|
# Convert a Python script to TypePHP |
||||||
|
./tpc --convert-python-to-php script.py > script.php |
||||||
``` |
``` |
||||||
|
|
||||||
Windows packaging requires `PHP_HOME` and `PHPX_HOME`; Linux packaging requires |
See [Python tool submodule](docs/python/tools.md). |
||||||
UPX; macOS uses `strip` when available. TypePHP rejects 32-bit targets and |
|
||||||
supports common 64-bit CPU architectures, including x86-64 and ARM64. |
## Documentation |
||||||
|
|
||||||
|
- [Quick Start](docs/QUICKSTART.md) — minimal compilation flow |
||||||
|
- [Compilation modes](docs/COMPILATION_MODES.md) — `bin`, `ext`, `lib` |
||||||
|
- [Compiler CLI](docs/COMPILER_CLI.md) — CLI arguments and project config |
||||||
|
- [Incompatible PHP features](docs/INCOMPATIBLE_PHP_FEATURES.md) — current limits |
||||||
|
- [Native types](docs/NATIVE_TYPES.md) — native scalar types |
||||||
|
- [High-precision types](docs/HIGH_PRECISION_TYPES.md) — BigInt / Decimal / BigFloat |
||||||
|
- [Std containers](docs/STD_CONTAINERS.md) — strongly-typed containers |
||||||
|
- [Universal methods](docs/UNIVERSAL_METHODS.md) — zero-overhead methods |
||||||
|
- [Compile-time functions](docs/COMPILE_TIME_FUNCTIONS.md) — `any()`, `refval()`, `objval()`, … |
||||||
|
- [Mixed C++/PHP](docs/MIXED_CPP_PHP.md) — C++/PHP interop |
||||||
|
- [`#[Immutable]`](docs/IMMUTABLE.md) — compile-time read-only contracts |
||||||
|
- [WASI build](docs/WASI_BUILD.md) — WASI targets |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
TypePHP is licensed under the [GNU General Public License v3.0](LICENSE). |
||||||
|
|
||||||
|
## Community |
||||||
|
|
||||||
|
- Repository: <https://github.com/swoole/typephp> |
||||||
|
- Copyright © 2026 上海识沃网络科技有限公司 (Swoole) |
||||||
|
|||||||
@ -1,52 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> |
|
||||||
|
|
||||||
<!-- 程序集标识 --> |
|
||||||
<assemblyIdentity |
|
||||||
type="win32" |
|
||||||
name="AotCompiler.App" |
|
||||||
version="1.0.0.0" /> |
|
||||||
|
|
||||||
<!-- 信任信息 / UAC 权限级别 --> |
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> |
|
||||||
<security> |
|
||||||
<requestedPrivileges> |
|
||||||
<!-- |
|
||||||
asInvoker: 以当前用户权限运行(推荐默认值) |
|
||||||
highestAvailable: 请求当前用户能获得的最高权限 |
|
||||||
requireAdministrator: 要求管理员权限 |
|
||||||
--> |
|
||||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" /> |
|
||||||
</requestedPrivileges> |
|
||||||
</security> |
|
||||||
</trustInfo> |
|
||||||
|
|
||||||
<!-- 应用程序兼容性 --> |
|
||||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> |
|
||||||
<application> |
|
||||||
<!-- Windows 10 / 11 --> |
|
||||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> |
|
||||||
<!-- Windows 8.1 --> |
|
||||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" /> |
|
||||||
<!-- Windows 8 --> |
|
||||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" /> |
|
||||||
<!-- Windows 7 --> |
|
||||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" /> |
|
||||||
</application> |
|
||||||
</compatibility> |
|
||||||
|
|
||||||
<!-- DPI 感知(Windows 10 版本 1607+) --> |
|
||||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> |
|
||||||
<asmv3:windowsSettings> |
|
||||||
<!-- |
|
||||||
PerMonitorV2: 逐显示器 DPI 感知 v2(推荐,支持混合模式 DPI 缩放) |
|
||||||
PerMonitor: 逐显示器 DPI 感知 v1 |
|
||||||
System: 系统 DPI 感知 |
|
||||||
None / 不设置: DPI 不感知(系统会自动缩放) |
|
||||||
--> |
|
||||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware> |
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> |
|
||||||
</asmv3:windowsSettings> |
|
||||||
</asmv3:application> |
|
||||||
|
|
||||||
</assembly> |
|
||||||
@ -1,495 +0,0 @@ |
|||||||
#!/usr/bin/env python3 |
|
||||||
""" |
|
||||||
使用 libclang 解析 C++ 代码并添加头文件路径 |
|
||||||
""" |
|
||||||
|
|
||||||
import clang.cindex |
|
||||||
from clang.cindex import Index, CursorKind, TypeKind, StorageClass |
|
||||||
import json |
|
||||||
import sys |
|
||||||
import os |
|
||||||
from pathlib import Path |
|
||||||
import subprocess |
|
||||||
import argparse |
|
||||||
|
|
||||||
|
|
||||||
class PHPConfigHelper: |
|
||||||
"""PHP 配置辅助类""" |
|
||||||
|
|
||||||
def __init__(self, php_config_path='php-config'): |
|
||||||
self.php_config = php_config_path |
|
||||||
self._check_availability() |
|
||||||
|
|
||||||
def _check_availability(self): |
|
||||||
"""检查 php-config 是否可用""" |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--version'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
print(f"✓ 找到 PHP {result.stdout.strip()}") |
|
||||||
except (FileNotFoundError, subprocess.CalledProcessError) as e: |
|
||||||
print(f"警告: php-config 不可用: {e}") |
|
||||||
# 尝试使用常见路径 |
|
||||||
common_paths = [ |
|
||||||
'/usr/bin/php-config', |
|
||||||
'/usr/local/bin/php-config', |
|
||||||
'/opt/php/bin/php-config' |
|
||||||
] |
|
||||||
for path in common_paths: |
|
||||||
if os.path.exists(path): |
|
||||||
self.php_config = path |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--version'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
print(f"✓ 找到 PHP {result.stdout.strip()} 在 {path}") |
|
||||||
return |
|
||||||
except (FileNotFoundError, subprocess.CalledProcessError): |
|
||||||
continue |
|
||||||
print("警告: php-config 在任何常见路径都不可用,使用默认路径") |
|
||||||
|
|
||||||
def get_includes(self): |
|
||||||
""" |
|
||||||
获取 include 路径列表 |
|
||||||
|
|
||||||
Returns: |
|
||||||
list: 头文件路径列表(不带 -I 前缀) |
|
||||||
""" |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--includes'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
|
|
||||||
# 解析输出: "-I/path1 -I/path2" -> ['/path1', '/path2'] |
|
||||||
includes = [] |
|
||||||
for flag in result.stdout.strip().split(): |
|
||||||
if flag.startswith('-I'): |
|
||||||
includes.append(flag[2:]) |
|
||||||
|
|
||||||
return includes |
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError): |
|
||||||
print("警告: 无法获取 PHP includes,使用默认路径") |
|
||||||
return ['/usr/include/php', '/usr/include/php/20210902'] # 默认路径 |
|
||||||
|
|
||||||
def get_include_dir(self): |
|
||||||
"""获取主 include 目录""" |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--include-dir'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
return result.stdout.strip() |
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError): |
|
||||||
return '/usr/include/php' |
|
||||||
|
|
||||||
def get_extension_dir(self): |
|
||||||
"""获取扩展目录""" |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--extension-dir'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
return result.stdout.strip() |
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError): |
|
||||||
return '/usr/lib/php' |
|
||||||
|
|
||||||
def get_version(self): |
|
||||||
"""获取 PHP 版本""" |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--version'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
return result.stdout.strip() |
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError): |
|
||||||
return 'unknown' |
|
||||||
|
|
||||||
def get_php_binary(self): |
|
||||||
"""获取 PHP 二进制路径""" |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--php-binary'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
return result.stdout.strip() |
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError): |
|
||||||
return 'php' |
|
||||||
|
|
||||||
def get_configure_options(self): |
|
||||||
"""获取配置选项""" |
|
||||||
try: |
|
||||||
result = subprocess.run( |
|
||||||
[self.php_config, '--configure-options'], |
|
||||||
capture_output=True, |
|
||||||
text=True, |
|
||||||
check=True |
|
||||||
) |
|
||||||
return result.stdout.strip() |
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError): |
|
||||||
return '' |
|
||||||
|
|
||||||
def get_all_info(self): |
|
||||||
"""获取所有配置信息""" |
|
||||||
return { |
|
||||||
'version': self.get_version(), |
|
||||||
'includes': self.get_includes(), |
|
||||||
'include_dir': self.get_include_dir(), |
|
||||||
'extension_dir': self.get_extension_dir(), |
|
||||||
'php_binary': self.get_php_binary(), |
|
||||||
'configure_options': self.get_configure_options(), |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
class ClangParser: |
|
||||||
def __init__(self, libclang_path=None): |
|
||||||
""" |
|
||||||
初始化 Clang 解析器 |
|
||||||
|
|
||||||
Args: |
|
||||||
libclang_path: libclang 库的路径(可选) |
|
||||||
""" |
|
||||||
if libclang_path: |
|
||||||
try: |
|
||||||
clang.cindex.Config.set_library_file(libclang_path) |
|
||||||
except Exception as e: |
|
||||||
print(f"警告: 无法设置 libclang 路径 {libclang_path}: {e}") |
|
||||||
print("尝试使用默认路径...") |
|
||||||
|
|
||||||
try: |
|
||||||
self.index = Index.create() |
|
||||||
except Exception as e: |
|
||||||
print(f"错误: 无法创建 Clang 索引: {e}") |
|
||||||
print("请确保已安装 python3-clang 和 clang 库") |
|
||||||
raise |
|
||||||
|
|
||||||
def parse_file(self, filename, include_paths=None, defines=None, |
|
||||||
compiler_args=None, language='c++'): |
|
||||||
""" |
|
||||||
解析 C++ 文件 |
|
||||||
|
|
||||||
Args: |
|
||||||
filename: 要解析的文件路径 |
|
||||||
include_paths: 头文件搜索路径列表 |
|
||||||
defines: 宏定义列表 ['MACRO=value', 'DEBUG'] |
|
||||||
compiler_args: 额外的编译器参数 |
|
||||||
language: 语言类型 ('c', 'c++', 'objective-c') |
|
||||||
|
|
||||||
Returns: |
|
||||||
TranslationUnit 对象 |
|
||||||
""" |
|
||||||
if not os.path.exists(filename): |
|
||||||
raise FileNotFoundError(f"文件不存在: {filename}") |
|
||||||
|
|
||||||
args = [] |
|
||||||
|
|
||||||
# 1. 设置语言标准 |
|
||||||
if language == 'c++': |
|
||||||
args.extend([ |
|
||||||
'-x', 'c++', |
|
||||||
'-std=c++14', # 更标准的 C++ 版本 |
|
||||||
]) |
|
||||||
elif language == 'c': |
|
||||||
args.extend(['-x', 'c', '-std=c11']) |
|
||||||
|
|
||||||
# 2. 添加头文件搜索路径 |
|
||||||
if include_paths: |
|
||||||
for path in include_paths: |
|
||||||
if os.path.exists(path): # 检查路径是否存在 |
|
||||||
args.append(f'-I{path}') |
|
||||||
else: |
|
||||||
print(f"警告: 包含路径不存在: {path}") |
|
||||||
|
|
||||||
# 3. 添加宏定义 |
|
||||||
if defines: |
|
||||||
for define in defines: |
|
||||||
args.append(f'-D{define}') |
|
||||||
|
|
||||||
# 4. 添加额外的编译器参数 |
|
||||||
if compiler_args: |
|
||||||
args.extend(compiler_args) |
|
||||||
|
|
||||||
# 5. 常用的编译选项 |
|
||||||
args.extend([ |
|
||||||
'-Wno-pragma-once-outside-header', # 忽略警告 |
|
||||||
'-ferror-limit=0', # 不限制错误数量 |
|
||||||
'-fno-delayed-template-parsing', # 避免某些 C++ 模板解析问题 |
|
||||||
'-w', # 禁用所有警告以减少输出 |
|
||||||
]) |
|
||||||
|
|
||||||
print(f"编译参数: {' '.join(args)}") |
|
||||||
|
|
||||||
# 解析文件 |
|
||||||
try: |
|
||||||
tu = self.index.parse( |
|
||||||
filename, |
|
||||||
args=args, |
|
||||||
options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
|
||||||
) |
|
||||||
except Exception as e: |
|
||||||
print(f"解析文件时出错: {e}") |
|
||||||
print("尝试使用最小参数集...") |
|
||||||
# 尝试使用最小参数集 |
|
||||||
minimal_args = ['-x', 'c++', '-std=c++14', '-w'] |
|
||||||
if include_paths: |
|
||||||
for path in include_paths: |
|
||||||
if os.path.exists(path): |
|
||||||
minimal_args.append(f'-I{path}') |
|
||||||
try: |
|
||||||
tu = self.index.parse( |
|
||||||
filename, |
|
||||||
args=minimal_args, |
|
||||||
options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
|
||||||
) |
|
||||||
print("使用最小参数集成功解析") |
|
||||||
except Exception as e2: |
|
||||||
print(f"使用最小参数集也失败: {e2}") |
|
||||||
print("尝试解析不包含头文件的简化版本...") |
|
||||||
# 创建一个临时文件,移除头文件包含行 |
|
||||||
temp_filename = filename + ".tmp" |
|
||||||
with open(filename, 'r') as original: |
|
||||||
lines = original.readlines() |
|
||||||
|
|
||||||
# 移除 #include 行 |
|
||||||
filtered_lines = [line for line in lines if not line.strip().startswith('#include')] |
|
||||||
|
|
||||||
with open(temp_filename, 'w') as temp: |
|
||||||
temp.writelines(filtered_lines) |
|
||||||
|
|
||||||
try: |
|
||||||
tu = self.index.parse( |
|
||||||
temp_filename, |
|
||||||
args=minimal_args, |
|
||||||
options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD |
|
||||||
) |
|
||||||
print("解析简化版本成功") |
|
||||||
# 清理临时文件 |
|
||||||
os.remove(temp_filename) |
|
||||||
except Exception as e3: |
|
||||||
print(f"简化版本也失败: {e3}") |
|
||||||
# 清理临时文件 |
|
||||||
if os.path.exists(temp_filename): |
|
||||||
os.remove(temp_filename) |
|
||||||
raise |
|
||||||
|
|
||||||
# 检查诊断信息 |
|
||||||
if tu.diagnostics: |
|
||||||
print(f"\n诊断信息 ({len(tu.diagnostics)} 个):") |
|
||||||
error_count = 0 |
|
||||||
warning_count = 0 |
|
||||||
|
|
||||||
for diag in tu.diagnostics: |
|
||||||
if diag.severity >= 3: # 错误级别 |
|
||||||
error_count += 1 |
|
||||||
else: # 警告级别 |
|
||||||
warning_count += 1 |
|
||||||
|
|
||||||
print(f"错误: {error_count}, 警告: {warning_count}") |
|
||||||
|
|
||||||
# 只显示前几个诊断信息,避免输出过多 |
|
||||||
for i, diag in enumerate(tu.diagnostics): |
|
||||||
if i >= 5: # 只显示前5个 |
|
||||||
print("... 还有更多诊断信息") |
|
||||||
break |
|
||||||
print(f" [{diag.severity}] {diag.spelling}") |
|
||||||
if diag.location.file: |
|
||||||
print(f" at {diag.location.file.name}:{diag.location.line}") |
|
||||||
|
|
||||||
return tu |
|
||||||
|
|
||||||
def extract_functions(self, tu, name_prefixes=None): |
|
||||||
""" |
|
||||||
提取函数定义 |
|
||||||
|
|
||||||
Args: |
|
||||||
tu: TranslationUnit 对象 |
|
||||||
name_prefixes: 函数名前缀过滤列表 |
|
||||||
|
|
||||||
Returns: |
|
||||||
函数信息列表 |
|
||||||
""" |
|
||||||
functions = [] |
|
||||||
|
|
||||||
def visit_node(node, depth=0): |
|
||||||
# 只处理函数声明/定义 |
|
||||||
if node.kind == CursorKind.FUNCTION_DECL: |
|
||||||
try: |
|
||||||
func_info = self.parse_function(node) |
|
||||||
|
|
||||||
# 过滤函数名 |
|
||||||
if name_prefixes: |
|
||||||
if any(func_info['name'].startswith(prefix) |
|
||||||
for prefix in name_prefixes): |
|
||||||
functions.append(func_info) |
|
||||||
else: |
|
||||||
functions.append(func_info) |
|
||||||
except Exception as e: |
|
||||||
print(f"解析函数时出错: {e}") |
|
||||||
|
|
||||||
# 递归访问子节点 |
|
||||||
for child in node.get_children(): |
|
||||||
visit_node(child, depth + 1) |
|
||||||
|
|
||||||
visit_node(tu.cursor) |
|
||||||
return functions |
|
||||||
|
|
||||||
def parse_function(self, cursor): |
|
||||||
""" |
|
||||||
解析函数详细信息 |
|
||||||
""" |
|
||||||
# 检查方法是否存在 |
|
||||||
def safe_call(method, default_value=None): |
|
||||||
try: |
|
||||||
return method() |
|
||||||
except AttributeError: |
|
||||||
return default_value |
|
||||||
|
|
||||||
# 基本信息 |
|
||||||
func_info = { |
|
||||||
'name': cursor.spelling, |
|
||||||
'displayName': cursor.displayname, |
|
||||||
'mangledName': cursor.mangled_name, |
|
||||||
'returnType': cursor.result_type.spelling, |
|
||||||
'isStatic': cursor.storage_class == StorageClass.STATIC, |
|
||||||
'isInline': safe_call(lambda: cursor.is_inline_function(), False), |
|
||||||
'isVirtual': safe_call(lambda: cursor.is_virtual_method(), False), |
|
||||||
'isConst': safe_call(lambda: cursor.is_const_method(), False), |
|
||||||
'location': { |
|
||||||
'file': str(cursor.location.file) if cursor.location.file else None, |
|
||||||
'line': cursor.location.line, |
|
||||||
'column': cursor.location.column, |
|
||||||
}, |
|
||||||
'parameters': [], |
|
||||||
'namespaces': self.get_namespaces(cursor), |
|
||||||
} |
|
||||||
|
|
||||||
# 解析参数 |
|
||||||
for arg in cursor.get_arguments(): |
|
||||||
param_info = { |
|
||||||
'name': arg.spelling or f'arg{len(func_info["parameters"])}', |
|
||||||
'type': arg.type.spelling, |
|
||||||
'canonicalType': arg.type.get_canonical().spelling, |
|
||||||
} |
|
||||||
|
|
||||||
# 检查是否有默认值 |
|
||||||
try: |
|
||||||
for token in arg.get_tokens(): |
|
||||||
if token.spelling == '=': |
|
||||||
# 有默认值 |
|
||||||
param_info['hasDefault'] = True |
|
||||||
break |
|
||||||
except: |
|
||||||
# 如果无法获取 tokens,跳过默认值检查 |
|
||||||
pass |
|
||||||
|
|
||||||
func_info['parameters'].append(param_info) |
|
||||||
|
|
||||||
return func_info |
|
||||||
|
|
||||||
def get_namespaces(self, cursor): |
|
||||||
""" |
|
||||||
获取函数所在的命名空间 |
|
||||||
""" |
|
||||||
namespaces = [] |
|
||||||
parent = cursor.semantic_parent |
|
||||||
|
|
||||||
while parent and parent.kind != CursorKind.TRANSLATION_UNIT: |
|
||||||
if parent.kind == CursorKind.NAMESPACE: |
|
||||||
namespaces.insert(0, parent.spelling) |
|
||||||
parent = parent.semantic_parent |
|
||||||
|
|
||||||
return namespaces |
|
||||||
|
|
||||||
|
|
||||||
def main(): |
|
||||||
parser = argparse.ArgumentParser(description='使用 libclang 解析 C++ 代码并提取函数信息') |
|
||||||
parser.add_argument('filename', help='要解析的 C++ 文件路径') |
|
||||||
parser.add_argument('--libclang-path', help='libclang 库路径') |
|
||||||
parser.add_argument('--include-paths', nargs='*', help='额外的包含路径') |
|
||||||
parser.add_argument('--function-prefixes', nargs='*', help='函数名前缀过滤器') |
|
||||||
|
|
||||||
args = parser.parse_args() |
|
||||||
|
|
||||||
if not os.path.exists(args.filename): |
|
||||||
print(f"错误: 文件不存在: {args.filename}") |
|
||||||
sys.exit(1) |
|
||||||
|
|
||||||
try: |
|
||||||
# 创建解析器 |
|
||||||
parser_obj = ClangParser(libclang_path=args.libclang_path) |
|
||||||
|
|
||||||
# 配置头文件路径 |
|
||||||
include_paths = args.include_paths or [ |
|
||||||
"/usr/include/linux", |
|
||||||
"/home/swoole/workspace/projects/phpx/include" |
|
||||||
] |
|
||||||
|
|
||||||
# 尝试获取 PHP 配置的头文件路径 |
|
||||||
try: |
|
||||||
php_config = PHPConfigHelper() |
|
||||||
php_includes = php_config.get_includes() |
|
||||||
include_paths.extend(php_includes) |
|
||||||
except Exception as e: |
|
||||||
print(f"警告: 无法获取 PHP 配置: {e}") |
|
||||||
print("继续使用默认路径...") |
|
||||||
|
|
||||||
# 配置宏定义 |
|
||||||
defines = [ |
|
||||||
'HAVE_CONFIG_H', |
|
||||||
'ZEND_ENABLE_STATIC_TSRMLS_CACHE=1', |
|
||||||
] |
|
||||||
|
|
||||||
# 额外的编译器参数 |
|
||||||
compiler_args = [ |
|
||||||
'-fparse-all-comments', # 解析所有注释 |
|
||||||
'-Wno-unknown-pragmas', |
|
||||||
] |
|
||||||
|
|
||||||
# 解析文件 |
|
||||||
tu = parser_obj.parse_file( |
|
||||||
args.filename, |
|
||||||
include_paths=include_paths, |
|
||||||
defines=defines, |
|
||||||
compiler_args=compiler_args, |
|
||||||
language='c++' |
|
||||||
) |
|
||||||
|
|
||||||
# 提取函数 |
|
||||||
name_prefixes = args.function_prefixes or None |
|
||||||
functions = parser_obj.extract_functions(tu, name_prefixes=name_prefixes) |
|
||||||
|
|
||||||
# 输出结果 |
|
||||||
output = { |
|
||||||
'file': args.filename, |
|
||||||
'functions': functions, |
|
||||||
'total': len(functions), |
|
||||||
} |
|
||||||
|
|
||||||
print(json.dumps(output, indent=2, ensure_ascii=False)) |
|
||||||
|
|
||||||
except clang.cindex.TranslationUnitLoadError as e: |
|
||||||
print(f"翻译单元加载错误: {e}") |
|
||||||
print("这通常意味着 C++ 代码包含语法错误或缺少必要的头文件") |
|
||||||
sys.exit(1) |
|
||||||
except Exception as e: |
|
||||||
print(f"错误: {e}") |
|
||||||
sys.exit(1) |
|
||||||
|
|
||||||
if __name__ == '__main__': |
|
||||||
main() |
|
||||||
@ -0,0 +1,146 @@ |
|||||||
|
#!/usr/bin/env php |
||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of Swoole-Compiler(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
use TypePhp\Testing\TestCoverageAnalyzer; |
||||||
|
|
||||||
|
require __DIR__ . '/bootstrap.php'; |
||||||
|
|
||||||
|
$format = 'summary'; |
||||||
|
$output = null; |
||||||
|
$includePhpUnit = true; |
||||||
|
$strict = false; |
||||||
|
$phpVersions = ['8.4', '8.5']; |
||||||
|
$paths = []; |
||||||
|
|
||||||
|
foreach (array_slice($argv, 1) as $argument) { |
||||||
|
if ($argument === '--help' || $argument === '-h') { |
||||||
|
printUsage($argv[0]); |
||||||
|
exit(0); |
||||||
|
} |
||||||
|
if ($argument === '--no-phpunit') { |
||||||
|
$includePhpUnit = false; |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($argument === '--strict') { |
||||||
|
$strict = true; |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (str_starts_with($argument, '--format=')) { |
||||||
|
$format = substr($argument, strlen('--format=')); |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (str_starts_with($argument, '--output=')) { |
||||||
|
$output = substr($argument, strlen('--output=')); |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (str_starts_with($argument, '--php-versions=')) { |
||||||
|
$phpVersions = array_values(array_filter(array_map('trim', explode(',', substr($argument, strlen('--php-versions=')))))); |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (str_starts_with($argument, '-')) { |
||||||
|
fwrite(STDERR, 'Unknown option: ' . $argument . PHP_EOL); |
||||||
|
exit(2); |
||||||
|
} |
||||||
|
$paths[] = $argument; |
||||||
|
} |
||||||
|
|
||||||
|
if (!in_array($format, ['summary', 'json', 'markdown'], true)) { |
||||||
|
fwrite(STDERR, 'Invalid format. Expected summary, json or markdown.' . PHP_EOL); |
||||||
|
exit(2); |
||||||
|
} |
||||||
|
if ($phpVersions === []) { |
||||||
|
fwrite(STDERR, 'At least one target PHP version is required.' . PHP_EOL); |
||||||
|
exit(2); |
||||||
|
} |
||||||
|
foreach ($phpVersions as $version) { |
||||||
|
if (!preg_match('/^\d+\.\d+$/', $version)) { |
||||||
|
fwrite(STDERR, 'Invalid PHP version: ' . $version . PHP_EOL); |
||||||
|
exit(2); |
||||||
|
} |
||||||
|
} |
||||||
|
if ($paths === []) { |
||||||
|
$paths = ['tests/compiler']; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
$analyzer = new TestCoverageAnalyzer(ROOT_PATH, $phpVersions); |
||||||
|
$report = $analyzer->analyze( |
||||||
|
$paths, |
||||||
|
$includePhpUnit ? ROOT_PATH . '/phpunit/src' : null, |
||||||
|
$includePhpUnit ? ROOT_PATH . '/phpunit/code' : null, |
||||||
|
); |
||||||
|
} catch (Throwable $error) { |
||||||
|
fwrite(STDERR, 'Coverage analysis failed: ' . $error->getMessage() . PHP_EOL); |
||||||
|
exit(1); |
||||||
|
} |
||||||
|
|
||||||
|
$rendered = match ($format) { |
||||||
|
'summary' => $analyzer->renderSummary($report), |
||||||
|
'markdown' => $analyzer->renderMarkdown($report), |
||||||
|
'json' => json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . PHP_EOL, |
||||||
|
}; |
||||||
|
|
||||||
|
if ($output === null) { |
||||||
|
echo $rendered; |
||||||
|
} else { |
||||||
|
$outputPath = isAbsolutePath($output) ? $output : ROOT_PATH . DIRECTORY_SEPARATOR . $output; |
||||||
|
$directory = dirname($outputPath); |
||||||
|
if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) { |
||||||
|
fwrite(STDERR, 'Unable to create output directory: ' . $directory . PHP_EOL); |
||||||
|
exit(1); |
||||||
|
} |
||||||
|
if (file_put_contents($outputPath, $rendered) === false) { |
||||||
|
fwrite(STDERR, 'Unable to write report: ' . $outputPath . PHP_EOL); |
||||||
|
exit(1); |
||||||
|
} |
||||||
|
echo 'Wrote ', $format, ' coverage report: ', relativePath(ROOT_PATH, $outputPath), PHP_EOL; |
||||||
|
} |
||||||
|
|
||||||
|
if ($strict && ($report['parse_errors'] !== [] || $report['unresolved_phpunit_fixtures'] !== [])) { |
||||||
|
exit(1); |
||||||
|
} |
||||||
|
|
||||||
|
function printUsage(string $script): void |
||||||
|
{ |
||||||
|
echo <<<USAGE |
||||||
|
Usage: |
||||||
|
php {$script} [options] [PHPT path ...] |
||||||
|
|
||||||
|
Options: |
||||||
|
--format=summary|json|markdown Output format (default: summary) |
||||||
|
--output=<file> Write the report to a file |
||||||
|
--php-versions=8.4,8.5 Target PHP version columns |
||||||
|
--no-phpunit Do not scan PHPUnit compiler fixtures |
||||||
|
--strict Fail on parse issues or unresolved fixture links |
||||||
|
-h, --help Show this help |
||||||
|
|
||||||
|
Examples: |
||||||
|
php {$script} |
||||||
|
php {$script} --format=markdown --output=build/test-coverage.md |
||||||
|
php {$script} --format=json tests/compiler/type_decl tests/compiler/basic |
||||||
|
|
||||||
|
The tool reports separate, explicitly denominated AST-node, positive compile, |
||||||
|
runtime semantic and negative diagnostic coverage. It never emits a combined |
||||||
|
overall percentage. |
||||||
|
|
||||||
|
USAGE; |
||||||
|
} |
||||||
|
|
||||||
|
function isAbsolutePath(string $path): bool |
||||||
|
{ |
||||||
|
return $path !== '' && ($path[0] === '/' || preg_match('/^[A-Za-z]:[\\\\\/]/', $path) === 1); |
||||||
|
} |
||||||
|
|
||||||
|
function relativePath(string $root, string $path): string |
||||||
|
{ |
||||||
|
$prefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; |
||||||
|
return str_starts_with($path, $prefix) ? substr($path, strlen($prefix)) : $path; |
||||||
|
} |
||||||
@ -0,0 +1,178 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -euo pipefail |
||||||
|
|
||||||
|
readonly MIN_AGE_MINUTES=60 |
||||||
|
|
||||||
|
dry_run=false |
||||||
|
tmp_root=/tmp |
||||||
|
|
||||||
|
usage() { |
||||||
|
cat <<'EOF' |
||||||
|
Usage: ./cleanup-typephp-tmp.sh [options] |
||||||
|
|
||||||
|
Remove inactive TypePHP temporary files and directories from /tmp. |
||||||
|
An entry is skipped when it or any of its descendants was modified or |
||||||
|
metadata-changed during the last 60 minutes. |
||||||
|
Recognized prefixes: typephp-, typephp_, utils_test_, and phpx-windows. |
||||||
|
|
||||||
|
Options: |
||||||
|
-n, --dry-run Show what would be removed without deleting anything |
||||||
|
--tmp-dir DIR Use another temporary directory (primarily for testing) |
||||||
|
-h, --help Show this help |
||||||
|
EOF |
||||||
|
} |
||||||
|
|
||||||
|
while (($# > 0)); do |
||||||
|
case "$1" in |
||||||
|
-n | --dry-run) |
||||||
|
dry_run=true |
||||||
|
;; |
||||||
|
--tmp-dir) |
||||||
|
if (($# < 2)); then |
||||||
|
echo "Error: --tmp-dir requires a directory." >&2 |
||||||
|
exit 2 |
||||||
|
fi |
||||||
|
tmp_root=$2 |
||||||
|
shift |
||||||
|
;; |
||||||
|
-h | --help) |
||||||
|
usage |
||||||
|
exit 0 |
||||||
|
;; |
||||||
|
*) |
||||||
|
echo "Error: unknown option: $1" >&2 |
||||||
|
usage >&2 |
||||||
|
exit 2 |
||||||
|
;; |
||||||
|
esac |
||||||
|
shift |
||||||
|
done |
||||||
|
|
||||||
|
if [[ ! -d "$tmp_root" ]]; then |
||||||
|
echo "Error: temporary directory does not exist: $tmp_root" >&2 |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
tmp_root=$(realpath -e -- "$tmp_root") |
||||||
|
if [[ -z "$tmp_root" || "$tmp_root" == / ]]; then |
||||||
|
echo "Error: refusing to use an unsafe temporary directory." >&2 |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
readonly tmp_root |
||||||
|
readonly owner_uid=${SUDO_UID:-$(id -u)} |
||||||
|
|
||||||
|
format_size() { |
||||||
|
local kib=$1 |
||||||
|
awk -v kib="$kib" 'BEGIN { |
||||||
|
if (kib >= 1048576) { |
||||||
|
printf "%.2f GiB", kib / 1048576 |
||||||
|
} else if (kib >= 1024) { |
||||||
|
printf "%.2f MiB", kib / 1024 |
||||||
|
} else { |
||||||
|
printf "%d KiB", kib |
||||||
|
} |
||||||
|
}' |
||||||
|
} |
||||||
|
|
||||||
|
entry_size_kib() { |
||||||
|
local output |
||||||
|
output=$(du -sk -- "$1" 2>/dev/null) || { |
||||||
|
printf '0' |
||||||
|
return |
||||||
|
} |
||||||
|
printf '%s' "${output%%$'\t'*}" |
||||||
|
} |
||||||
|
|
||||||
|
has_recent_entry() { |
||||||
|
local candidate=$1 |
||||||
|
local recent |
||||||
|
|
||||||
|
# Check the complete tree. Looking only at the top-level directory mtime |
||||||
|
# would miss writes to an existing file in a nested build directory. |
||||||
|
if ! recent=$(find -P "$candidate" -xdev \ |
||||||
|
\( -mmin "-${MIN_AGE_MINUTES}" -o -cmin "-${MIN_AGE_MINUTES}" \) \ |
||||||
|
-printf '1' -quit 2>/dev/null); then |
||||||
|
return 0 |
||||||
|
fi |
||||||
|
|
||||||
|
[[ -n "$recent" ]] |
||||||
|
} |
||||||
|
|
||||||
|
matched_count=0 |
||||||
|
removed_count=0 |
||||||
|
skipped_recent_count=0 |
||||||
|
skipped_error_count=0 |
||||||
|
total_kib=0 |
||||||
|
|
||||||
|
while IFS= read -r -d '' candidate; do |
||||||
|
((matched_count += 1)) |
||||||
|
|
||||||
|
# Keep the target constrained to one direct child of the selected root. |
||||||
|
if [[ "$candidate" != "$tmp_root"/* || "${candidate%/*}" != "$tmp_root" ]]; then |
||||||
|
echo "[skip unsafe] $candidate" >&2 |
||||||
|
((skipped_error_count += 1)) |
||||||
|
continue |
||||||
|
fi |
||||||
|
|
||||||
|
if has_recent_entry "$candidate"; then |
||||||
|
echo "[skip recent] $candidate" |
||||||
|
((skipped_recent_count += 1)) |
||||||
|
continue |
||||||
|
fi |
||||||
|
|
||||||
|
size_kib=$(entry_size_kib "$candidate") |
||||||
|
size=$(format_size "$size_kib") |
||||||
|
|
||||||
|
# The size scan can take noticeable time for a large build tree. Recheck |
||||||
|
# freshness immediately before acting in case a compiler started using it. |
||||||
|
if has_recent_entry "$candidate"; then |
||||||
|
echo "[skip recent] $candidate" |
||||||
|
((skipped_recent_count += 1)) |
||||||
|
continue |
||||||
|
fi |
||||||
|
|
||||||
|
if $dry_run; then |
||||||
|
echo "[would remove] $size $candidate" |
||||||
|
((removed_count += 1)) |
||||||
|
((total_kib += size_kib)) |
||||||
|
continue |
||||||
|
fi |
||||||
|
|
||||||
|
if [[ -d "$candidate" && ! -L "$candidate" ]]; then |
||||||
|
if rm -rf --one-file-system -- "$candidate"; then |
||||||
|
echo "[removed] $size $candidate" |
||||||
|
((removed_count += 1)) |
||||||
|
((total_kib += size_kib)) |
||||||
|
else |
||||||
|
echo "[skip error] failed to remove: $candidate" >&2 |
||||||
|
((skipped_error_count += 1)) |
||||||
|
fi |
||||||
|
elif rm -f -- "$candidate"; then |
||||||
|
echo "[removed] $size $candidate" |
||||||
|
((removed_count += 1)) |
||||||
|
((total_kib += size_kib)) |
||||||
|
else |
||||||
|
echo "[skip error] failed to remove: $candidate" >&2 |
||||||
|
((skipped_error_count += 1)) |
||||||
|
fi |
||||||
|
done < <( |
||||||
|
find -P "$tmp_root" -mindepth 1 -maxdepth 1 -uid "$owner_uid" \ |
||||||
|
\( -name 'typephp-*' -o -name 'typephp_*' \ |
||||||
|
-o -name 'utils_test_*' -o -name 'phpx-windows*' \) -print0 |
||||||
|
) |
||||||
|
|
||||||
|
if $dry_run; then |
||||||
|
action='would remove' |
||||||
|
else |
||||||
|
action='removed' |
||||||
|
fi |
||||||
|
|
||||||
|
printf 'Summary: matched %d, %s %d (%s), skipped recent %d, errors %d.\n' \ |
||||||
|
"$matched_count" "$action" "$removed_count" "$(format_size "$total_kib")" \ |
||||||
|
"$skipped_recent_count" "$skipped_error_count" |
||||||
|
|
||||||
|
if ((skipped_error_count > 0)); then |
||||||
|
exit 1 |
||||||
|
fi |
||||||
@ -0,0 +1,151 @@ |
|||||||
|
# Bash completion for the TypePHP compiler. |
||||||
|
# Generated by: tpc --generate-completion=bash |
||||||
|
|
||||||
|
_typephp_tpc_complete_files() |
||||||
|
{ |
||||||
|
local candidate |
||||||
|
COMPREPLY=() |
||||||
|
while IFS= read -r candidate; do |
||||||
|
if [[ -d "$candidate" || "$candidate" == *.php || "$candidate" == *.yml || "$candidate" == *.yaml || "$candidate" == *.prof ]]; then |
||||||
|
COMPREPLY+=("$candidate") |
||||||
|
fi |
||||||
|
done < <(compgen -f -- "$1") |
||||||
|
} |
||||||
|
|
||||||
|
_typephp_tpc_complete_python_files() |
||||||
|
{ |
||||||
|
local candidate |
||||||
|
COMPREPLY=() |
||||||
|
while IFS= read -r candidate; do |
||||||
|
if [[ -d "$candidate" || "$candidate" == *.py ]]; then |
||||||
|
COMPREPLY+=("$candidate") |
||||||
|
fi |
||||||
|
done < <(compgen -f -- "$1") |
||||||
|
} |
||||||
|
|
||||||
|
_typephp_tpc_complete_paths() |
||||||
|
{ |
||||||
|
local mode="$1" candidate |
||||||
|
COMPREPLY=() |
||||||
|
shift |
||||||
|
while IFS= read -r candidate; do |
||||||
|
COMPREPLY+=("$candidate") |
||||||
|
done < <(compgen "$mode" -- "$1") |
||||||
|
} |
||||||
|
|
||||||
|
_typephp_tpc() |
||||||
|
{ |
||||||
|
local current previous value candidate |
||||||
|
current="${COMP_WORDS[COMP_CWORD]}" |
||||||
|
previous="" |
||||||
|
if (( COMP_CWORD > 0 )); then |
||||||
|
previous="${COMP_WORDS[COMP_CWORD - 1]}" |
||||||
|
fi |
||||||
|
|
||||||
|
local index |
||||||
|
for ((index = 1; index < COMP_CWORD; index++)); do |
||||||
|
if [[ "${COMP_WORDS[index]}" == -- ]]; then |
||||||
|
compopt -o default |
||||||
|
return |
||||||
|
fi |
||||||
|
done |
||||||
|
|
||||||
|
case "$previous" in |
||||||
|
-O) |
||||||
|
COMPREPLY=( $(compgen -W '0 1 2 3' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--optimize) |
||||||
|
COMPREPLY=( $(compgen -W '0 1 2 3' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
-m) |
||||||
|
COMPREPLY=( $(compgen -W 'bin lib ext' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--mode) |
||||||
|
COMPREPLY=( $(compgen -W 'bin lib ext' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--php-version) |
||||||
|
COMPREPLY=( $(compgen -W '8.4 8.5' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--cxx-std) |
||||||
|
COMPREPLY=( $(compgen -W 'c++17 c++20 c++23' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--sanitize) |
||||||
|
COMPREPLY=( $(compgen -W 'address undefined' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--build-dir|--output-dir|-I|--include-path|-L|--link-path) |
||||||
|
compopt -o filenames |
||||||
|
_typephp_tpc_complete_paths -d "$current" |
||||||
|
return |
||||||
|
;; |
||||||
|
--convert-python-to-php) |
||||||
|
compopt -o filenames |
||||||
|
_typephp_tpc_complete_python_files "$current" |
||||||
|
return |
||||||
|
;; |
||||||
|
-o|--output) |
||||||
|
compopt -o filenames |
||||||
|
_typephp_tpc_complete_paths -f "$current" |
||||||
|
return |
||||||
|
;; |
||||||
|
esac |
||||||
|
|
||||||
|
case "$current" in |
||||||
|
-O[0-3]) |
||||||
|
COMPREPLY=("$current") |
||||||
|
return |
||||||
|
;; |
||||||
|
-O*) |
||||||
|
value="${current#-O}" |
||||||
|
COMPREPLY=( $(compgen -W '0 1 2 3' -P '-O' -- "$value") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--wasm=*) |
||||||
|
value="${current#--wasm=}" |
||||||
|
COMPREPLY=( $(compgen -W 'component browser' -P '--wasm=' -- "$value") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--generate-completion=*) |
||||||
|
value="${current#--generate-completion=}" |
||||||
|
COMPREPLY=( $(compgen -W 'bash' -P '--generate-completion=' -- "$value") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
--build-dir=*) |
||||||
|
value="${current#--build-dir=}" |
||||||
|
compopt -o filenames |
||||||
|
COMPREPLY=() |
||||||
|
while IFS= read -r candidate; do |
||||||
|
COMPREPLY+=("--build-dir=${candidate}") |
||||||
|
done < <(compgen -d -- "$value") |
||||||
|
return |
||||||
|
;; |
||||||
|
--output-dir=*) |
||||||
|
value="${current#--output-dir=}" |
||||||
|
compopt -o filenames |
||||||
|
COMPREPLY=() |
||||||
|
while IFS= read -r candidate; do |
||||||
|
COMPREPLY+=("--output-dir=${candidate}") |
||||||
|
done < <(compgen -d -- "$value") |
||||||
|
return |
||||||
|
;; |
||||||
|
-* ) |
||||||
|
COMPREPLY=( $(compgen -W '-O --optimize -o --output -h --help -v --version --profile --no-literal-strings --php-version -f --force -m --mode -r --run --debug -j --job --no-console --sanitize --cxx-std --march --target-platform --no-color --build-dir --dry -I --include-path -D --define --no-progress --lto --format -l --link-lib -L --link-path --wasm --wasm= --gen-python-helper --convert-python-to-php --output-dir --output-dir= --build-dir= --generate-completion=' -- "$current") ) |
||||||
|
return |
||||||
|
;; |
||||||
|
esac |
||||||
|
|
||||||
|
compopt -o filenames |
||||||
|
_typephp_tpc_complete_files "$current" |
||||||
|
} |
||||||
|
|
||||||
|
complete -F _typephp_tpc tpc |
||||||
|
complete -F _typephp_tpc ./tpc |
||||||
|
complete -F _typephp_tpc tpc.php |
||||||
|
complete -F _typephp_tpc bin/tpc.php |
||||||
|
complete -F _typephp_tpc ./bin/tpc.php |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
# `#[ArrayDef]` compile-time array contracts |
||||||
|
|
||||||
|
`#[ArrayDef]` attaches key/value type information to a property declared |
||||||
|
exactly as `array`. It supports both Zend classes and `#[Native]` classes and |
||||||
|
has no runtime metadata or per-read overhead. |
||||||
|
|
||||||
|
```php |
||||||
|
class Index |
||||||
|
{ |
||||||
|
#[ArrayDef(Type::String)] |
||||||
|
public array $names = []; // list<string> |
||||||
|
|
||||||
|
#[ArrayDef(Type::Int, Type::String)] |
||||||
|
public array $labels = []; // map<int, string> |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
One argument defines a list value type. Two arguments define a map key type |
||||||
|
and value type. Map keys are restricted to `Type::Int` or `Type::String`. |
||||||
|
`ClassName::class` is therefore valid only as a list element type or as the |
||||||
|
second (value) argument of a map. |
||||||
|
|
||||||
|
For direct writes whose expression types are known, the compiler either emits |
||||||
|
the normal write unchanged or reports a fatal type error. An `any` key/value is |
||||||
|
checked with PHPX exact-type helpers at runtime. No coercive `intval()` or |
||||||
|
string conversion is performed. |
||||||
|
|
||||||
|
List writes support `[]` and non-negative integer indexes up to PHP's current |
||||||
|
append position. Indexed writes uniformly emit `php::safeArrayIndex(index, |
||||||
|
array)`. The helper uses `zend_hash_next_free_element()` and follows the |
||||||
|
initial-index rule of `zend_hash_next_index_insert()`, which remains correct |
||||||
|
when `unset()` has created holes or removed the highest numeric key. An index |
||||||
|
equal to that value behaves like the next `$array[]` append; earlier indexes |
||||||
|
may update or refill an element. Negative indexes and indexes beyond the append |
||||||
|
position fail at runtime. There is no AST special case for |
||||||
|
`property[count(property)]`. Maps do not support `[]` append writes. |
||||||
|
|
||||||
|
The contract intentionally applies only to direct element assignment lowered |
||||||
|
by TypePHP. Reads and in-place operators are unchanged. Values passed through |
||||||
|
dynamic functions, callbacks, Reflection, `eval()`, or other ZendVM escape |
||||||
|
paths are outside the contract and have undefined behavior from ArrayDef's |
||||||
|
perspective. |
||||||
@ -0,0 +1,356 @@ |
|||||||
|
# C++ 命名空间、前缀与符号 ABI 规则 |
||||||
|
|
||||||
|
本文是 TypePHP、PHPX 以及 TypePHP 生成代码的内部 C++ 命名规范。它解决以下问题: |
||||||
|
|
||||||
|
- 区分 TypePHP 运行时逻辑、PHPX ZendAPI 封装、项目私有实现和用户 PHP 符号; |
||||||
|
- 防止框架 helper 与用户定义的 PHP 函数或类方法生成相同的 C++ 符号; |
||||||
|
- 明确哪些名称属于稳定 ABI,哪些名称仅限单个生成项目内部使用; |
||||||
|
- 为新增 helper、缓存、入口函数和生成符号提供统一的命名决策。 |
||||||
|
|
||||||
|
## 1. 总体规则 |
||||||
|
|
||||||
|
| 命名域 | 含义 | 典型形式 | 可见范围 | ABI 属性 | |
||||||
|
| --- | --- | --- | --- | --- | |
||||||
|
| `typephp_` | TypePHP 独有的运行时或编译产物支持逻辑 | `typephp_call_parent_constructor()` | TypePHP/PHPX 运行时 | 内部或显式导出 ABI | |
||||||
|
| `php::` | 对 ZendAPI、zval、HashTable、call frame 等 PHP 运行时能力的 C++ 封装 | `php::deindirect()` | PHPX C++ API | PHPX API | |
||||||
|
| `typephp_<project>` | 单个编译项目的私有 C++ 命名空间 | `namespace typephp_tpc` | 当前生成项目 | 非公共 ABI | |
||||||
|
| `php_` | 用户 PHP 函数和类方法映射后的 C++ callable 符号 | `php_app__user__save()` | 链接器可见 | TypePHP/stub callable ABI | |
||||||
|
|
||||||
|
核心约束: |
||||||
|
|
||||||
|
1. 不得新增全局的框架 `php_*` helper。 |
||||||
|
2. 与 TypePHP 无关、只是包装 ZendAPI 的能力必须放入 `namespace php`。 |
||||||
|
3. TypePHP 独有且需要跨生成文件调用的逻辑使用 `typephp_` 前缀。 |
||||||
|
4. 只服务于一个编译项目的数据和函数放入 `typephp_<project>` 命名空间。 |
||||||
|
5. 全局 `php_*` callable 名称保留给用户 PHP 声明的编译 ABI。 |
||||||
|
|
||||||
|
## 2. `typephp_`:TypePHP 独有逻辑 |
||||||
|
|
||||||
|
`typephp_` 表示该 API 的语义由 TypePHP 定义,不是 ZendAPI 的一般性 C++ 包装。常见场景包括: |
||||||
|
|
||||||
|
- TypePHP 属性读写规则; |
||||||
|
- TypePHP 构造、克隆和父方法调用链; |
||||||
|
- TypePHP 编译期 Attribute 的运行时支持; |
||||||
|
- TypePHP Native Class、Property Hook 等专属运行时逻辑; |
||||||
|
- TypePHP embed runtime 的初始化和关闭入口。 |
||||||
|
|
||||||
|
示例: |
||||||
|
|
||||||
|
```cpp |
||||||
|
typephp_call_parent_constructor(object, constructor, args); |
||||||
|
typephp_call_parent_clone(object, clone_method); |
||||||
|
typephp_install_property_handlers(class_entry, handlers); |
||||||
|
typephp_write_property_scoped(object, member, value, scope); |
||||||
|
TYPEPHP_RUNTIME_INIT(project)(argc, argv); |
||||||
|
``` |
||||||
|
|
||||||
|
### 2.1 使用边界 |
||||||
|
|
||||||
|
- 该前缀是 TypePHP 内部 C/C++ 名称空间,不代表 PHP 用户函数。 |
||||||
|
- 新增 API 时应使用完整、可识别的 snake_case 名称,不能使用含义过宽的名称,例如 `typephp_call()`。 |
||||||
|
- 仅在一个 `.cc` 文件中使用的函数还应增加 `static` 或放入匿名命名空间。 |
||||||
|
- 需要跨动态库边界时,使用对应的导出宏;不需要导出的 helper 不应扩大符号可见性。 |
||||||
|
- 不要仅因为代码位于 `typephp_helper.h` 就使用 `typephp_`;判断依据是语义是否为 TypePHP 独有。 |
||||||
|
|
||||||
|
### 2.2 正反例 |
||||||
|
|
||||||
|
```cpp |
||||||
|
// 正确:TypePHP 独有的构造链语义。 |
||||||
|
typephp_call_parent_constructor(object, constructor, args); |
||||||
|
|
||||||
|
// 错误:只是将 INDIRECT zval 物化为普通值,并非 TypePHP 独有。 |
||||||
|
typephp_deindirect(value); |
||||||
|
|
||||||
|
// 正确:通用 Zend 值包装属于 PHPX。 |
||||||
|
php::deindirect(value); |
||||||
|
``` |
||||||
|
|
||||||
|
## 3. `php::`:ZendAPI 的 C++ 封装 |
||||||
|
|
||||||
|
`namespace php` 由 PHPX 提供,用于把 Zend 的 C API、宏、裸指针和手工资源管理封装为类型安全、RAII 友好的 C++ API。 |
||||||
|
|
||||||
|
这一命名域包含两类能力: |
||||||
|
|
||||||
|
1. PHP 值和运行时对象,如 `php::Var`、`php::Str`、`php::Array`、`php::Object`; |
||||||
|
2. ZendAPI 的安全包装,如符号查询、作用域管理、值转换、对象创建和调用。 |
||||||
|
|
||||||
|
示例: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Var value; |
||||||
|
php::Array arguments; |
||||||
|
|
||||||
|
auto plain = php::deindirect(value); |
||||||
|
auto called_ce = php::getCalledCe(this_); |
||||||
|
auto scope = php::getCallableScope(function, this_); |
||||||
|
auto create_object = php::getCreateObjectFn(class_entry); |
||||||
|
auto globals = php::globalsArray(); |
||||||
|
``` |
||||||
|
|
||||||
|
### 3.1 何时使用 `php::` |
||||||
|
|
||||||
|
满足以下条件时应放入 `namespace php`: |
||||||
|
|
||||||
|
- API 对任何 PHPX C++ 调用者都有意义; |
||||||
|
- API 的行为可以完全用 Zend/PHP 运行时语义解释; |
||||||
|
- API 不依赖 TypePHP AST、编译期 Attribute 或 TypePHP 特有语言规则; |
||||||
|
- API 的主要作用是隐藏 Zend 宏、裸 `zval *`、引用计数或异常检查。 |
||||||
|
|
||||||
|
### 3.2 禁止全局 `php_*` helper |
||||||
|
|
||||||
|
以下旧式写法是禁止的: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Var php_deindirect(const php::Var &value); |
||||||
|
php::Str php_get_called_class(php::Object &this_); |
||||||
|
zend_class_entry *php_get_called_ce(php::Object &this_); |
||||||
|
auto php_get_create_object_fn(zend_class_entry *ce); |
||||||
|
``` |
||||||
|
|
||||||
|
它们必须写成: |
||||||
|
|
||||||
|
```cpp |
||||||
|
namespace php { |
||||||
|
|
||||||
|
Var deindirect(const Var &value); |
||||||
|
Str getCalledClass(Object &this_); |
||||||
|
zend_class_entry *getCalledCe(Object &this_); |
||||||
|
auto getCreateObjectFn(zend_class_entry *ce); |
||||||
|
|
||||||
|
} // namespace php |
||||||
|
``` |
||||||
|
|
||||||
|
原因是用户可以合法声明: |
||||||
|
|
||||||
|
```php |
||||||
|
function deindirect(mixed $value): mixed {} |
||||||
|
function get_called_ce(): string {} |
||||||
|
function get_create_object_fn(): string {} |
||||||
|
``` |
||||||
|
|
||||||
|
这些 PHP 函数会生成 `php_deindirect`、`php_get_called_ce` 和 |
||||||
|
`php_get_create_object_fn`。如果 PHPX 也在全局定义同名 helper,可能在声明、重载解析或链接阶段发生冲突。 |
||||||
|
|
||||||
|
### 3.3 命名风格 |
||||||
|
|
||||||
|
PHPX C++ API 使用现有的 camelCase 风格: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::getCalledClass(); |
||||||
|
php::getClassEntrySafe(); |
||||||
|
php::getPersistentCache(); |
||||||
|
php::stdCreateObject(); |
||||||
|
``` |
||||||
|
|
||||||
|
不要把 Zend 的 snake_case 名称机械地保留为全局 C++ 名称。底层调用可以继续使用 Zend 原始 API,例如 `zend_objects_new()`,但对生成代码暴露的包装层应使用 `php::`。 |
||||||
|
|
||||||
|
## 4. `typephp_<project>`:项目私有命名空间 |
||||||
|
|
||||||
|
每个 TypePHP 编译项目拥有独立的 C++ 命名空间: |
||||||
|
|
||||||
|
```text |
||||||
|
typephp_<target-name> |
||||||
|
``` |
||||||
|
|
||||||
|
例如项目名为 `tpc`: |
||||||
|
|
||||||
|
```cpp |
||||||
|
namespace typephp_tpc { |
||||||
|
// Project-private generated state and helpers. |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
项目名中的 `-` 和 `*` 会转换为 `_`,其余字符必须满足编译器的 target identifier 校验。由于固定带有 `typephp_` 前缀,即使项目名以数字开头,最终 C++ namespace 仍是合法标识符。 |
||||||
|
|
||||||
|
### 4.1 应放入该命名空间的内容 |
||||||
|
|
||||||
|
- literal string 表和 `get_str()`; |
||||||
|
- class/function/property cache 表及其访问函数; |
||||||
|
- 当前项目的全局变量存储; |
||||||
|
- class entry、object handler 和默认属性模板; |
||||||
|
- module entry、MINIT/RINIT/RSHUTDOWN 辅助状态; |
||||||
|
- `module_init()`、`module_clean()` 等仅在生成 extension 文件内部调用的函数; |
||||||
|
- Python module cache 等项目级生成状态。 |
||||||
|
|
||||||
|
示意: |
||||||
|
|
||||||
|
```cpp |
||||||
|
namespace typephp_demo { |
||||||
|
|
||||||
|
static php::Str literal_strings[] = { |
||||||
|
php::Str{"hello"}, |
||||||
|
}; |
||||||
|
|
||||||
|
php::Str &get_str(uint32_t index) { |
||||||
|
return literal_strings[index]; |
||||||
|
} |
||||||
|
|
||||||
|
static THREAD_LOCAL zend_class_entry *class_map[8]; |
||||||
|
|
||||||
|
zend_class_entry *get_class(int id, const php::Str &name) { |
||||||
|
// Resolve and cache a symbol owned by this project. |
||||||
|
} |
||||||
|
|
||||||
|
static void module_init() { |
||||||
|
// Initialize this project's generated state. |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace typephp_demo |
||||||
|
``` |
||||||
|
|
||||||
|
### 4.2 可见性与 ABI |
||||||
|
|
||||||
|
- `typephp_<project>` 内的名称是实现细节,不是 library stub ABI。 |
||||||
|
- 可限制为 `static` 的对象和函数应继续标记为 `static`。 |
||||||
|
- 生成头文件可以声明必须跨 translation unit 使用的项目内部 accessor,但不应暴露底层数组或缓存表。 |
||||||
|
- 外部手写 C++ 代码不得依赖 literal index、cache index 或项目内部 storage 名称。 |
||||||
|
- 不同 TypePHP 项目可以链接到同一进程,因为相同的内部短名称位于不同的项目 namespace 中。 |
||||||
|
|
||||||
|
### 4.3 作用域优先于名称拼写 |
||||||
|
|
||||||
|
项目 namespace 中仍可能出现历史生成名称,例如: |
||||||
|
|
||||||
|
```cpp |
||||||
|
typephp_demo::php_class_entry_App_User |
||||||
|
``` |
||||||
|
|
||||||
|
虽然成员名以 `php_` 开头,但完整符号位于 `typephp_demo` 中,因此它属于项目私有实现,而不是第 5 节所述的全局用户 callable ABI。新增项目内部 helper 应优先使用不带 `php_` 的短名称,例如 `get_class()`、`get_func()` 和 `get_str()`。 |
||||||
|
|
||||||
|
## 5. `php_`:用户 PHP callable 的 C++ ABI |
||||||
|
|
||||||
|
全局 `php_` 前缀用于 TypePHP 将用户声明的 PHP 函数和类方法映射为 C++ callable 符号。这套命名同时被生成代码、library stub 和外部 C++ 实现使用,因此不能随意改变。 |
||||||
|
|
||||||
|
示例: |
||||||
|
|
||||||
|
```php |
||||||
|
namespace App; |
||||||
|
|
||||||
|
function greet(string $name): string {} |
||||||
|
|
||||||
|
class User |
||||||
|
{ |
||||||
|
public function save(): bool {} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
概念上的 C++ 符号为: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Str php_app__greet(php::Str name); |
||||||
|
php::Bool php_app__user__save(php::Object &this_); |
||||||
|
``` |
||||||
|
|
||||||
|
规则包括: |
||||||
|
|
||||||
|
- 使用 `php_` 标识“由 PHP 声明映射而来”; |
||||||
|
- PHP namespace、class 和 method/function 名经过规范化后组合; |
||||||
|
- `__` 是现有 ABI 的组合分隔符; |
||||||
|
- 实例方法的第一个参数是对象 `this_`; |
||||||
|
- stub、library 和消费方必须使用完全相同的映射规则。 |
||||||
|
|
||||||
|
### 5.1 为什么内部 helper 不能使用 `php_` |
||||||
|
|
||||||
|
`php_` 映射不是独立的保留关键字空间,而是用户 PHP 名称的机械 ABI。以下用户声明: |
||||||
|
|
||||||
|
```php |
||||||
|
function deindirect(mixed $value): mixed {} |
||||||
|
``` |
||||||
|
|
||||||
|
会自然生成: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Var php_deindirect(php::Var value); |
||||||
|
``` |
||||||
|
|
||||||
|
因此框架若定义全局 `php_deindirect()`,就侵占了用户符号空间。正确做法是 `php::deindirect()`。 |
||||||
|
|
||||||
|
### 5.2 组合冲突 |
||||||
|
|
||||||
|
由于当前 ABI 使用 `__` 组合 PHP namespace、class 和 callable 名,下列两个 PHP 声明可能映射到同一个 C++ 符号: |
||||||
|
|
||||||
|
```php |
||||||
|
function App\user__test(): void {} |
||||||
|
|
||||||
|
namespace App; |
||||||
|
class User |
||||||
|
{ |
||||||
|
public function test(): void {} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
编译器必须在预处理阶段检测这种情况并抛出 FatalError,不能通过覆盖、链接顺序或增加运行时分派来处理。修改映射分隔规则会破坏既有 stub/ABI,因此冲突必须由用户重命名解决。 |
||||||
|
|
||||||
|
### 5.3 入口符号例外 |
||||||
|
|
||||||
|
少量 C ABI/嵌入入口由生成器固定定义,不属于普通用户 callable。例如: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php_<project>_embed_get_module(); |
||||||
|
typephp_<project>_runtime_init(argc, argv); |
||||||
|
typephp_<project>_runtime_shutdown(); |
||||||
|
``` |
||||||
|
|
||||||
|
这些是 binary/library embed runtime 与当前项目 module entry 的连接点。定义和引用统一通过 |
||||||
|
`TYPEPHP_EMBED_GET_MODULE_FUNCTION()`、`TYPEPHP_RUNTIME_INIT_FUNCTION()`、 |
||||||
|
`TYPEPHP_RUNTIME_SHUTDOWN_FUNCTION()` 及对应的符号宏生成,风格与 Zend 的 |
||||||
|
`PHP_MINIT_FUNCTION()`/`PHP_MINIT()` 一致。最终符号包含项目名,不得作为通用 helper 命名模板。 |
||||||
|
|
||||||
|
### 5.4 多扩展进程中的公共运行时 |
||||||
|
|
||||||
|
TypePHP 扩展不得分别编译或静态链接包含进程级 Zend 状态的 PHPX 实现。Reflection handler、 |
||||||
|
`FiberGenerator` class entry、作用域和 Property Hook 运行时均由共享的 `libphpx` 唯一提供: |
||||||
|
|
||||||
|
- host 模式的 extension/library 必须链接 `libphpx.so`、`libphpx.dylib` 或 `phpx.dll`,不能回退到 `libphpx.a`; |
||||||
|
- Unix PHP extension 不链接 Embed `libphp.so`,Zend/PHP 符号由加载它的 SAPI 提供; |
||||||
|
- macOS extension 使用 `-undefined dynamic_lookup` 解析宿主符号; |
||||||
|
- binary 和独立 WASI 程序仍可以静态链接,因为每个进程或 Wasm 实例只有一份运行时。 |
||||||
|
|
||||||
|
`src/core/typephp_*.cc` 只承载 TypePHP 专属的 `typephp_*` 运行时;`php::` ZendAPI 包装应放在不带 |
||||||
|
`typephp_` 前缀的 core 源文件中,例如 `src/core/scope.cc`。 |
||||||
|
|
||||||
|
## 6. 名称选择流程 |
||||||
|
|
||||||
|
新增 C++ API 时按以下顺序判断: |
||||||
|
|
||||||
|
1. **它是否是用户 PHP 函数或类方法的编译本体?** |
||||||
|
- 是:使用既有 `php_` callable ABI 生成器,禁止手写另一套映射。 |
||||||
|
2. **它是否只服务于当前一个 TypePHP 项目?** |
||||||
|
- 是:放入 `typephp_<project>`,并尽可能使用 `static` 或私有 accessor。 |
||||||
|
3. **它是否实现 TypePHP 独有语义?** |
||||||
|
- 是:使用 `typephp_` 前缀。 |
||||||
|
4. **它是否只是对 Zend/PHP 运行时能力的 C++ 封装?** |
||||||
|
- 是:放入 `namespace php`,使用 PHPX camelCase 风格。 |
||||||
|
5. **以上都不是?** |
||||||
|
- 不应随意加入 `typephp_helper.h`;应重新确认所属模块和公共 API 边界。 |
||||||
|
|
||||||
|
## 7. 代码审查清单 |
||||||
|
|
||||||
|
新增或修改生成 helper 时必须检查: |
||||||
|
|
||||||
|
- [ ] `typephp_helper.h` 中没有新增全局 `php_*` helper; |
||||||
|
- [ ] ZendAPI 包装位于 `namespace php`; |
||||||
|
- [ ] TypePHP 独有逻辑使用 `typephp_`; |
||||||
|
- [ ] 项目缓存和 storage 位于 `typephp_<project>`; |
||||||
|
- [ ] 项目私有表没有通过生成头文件直接 `extern` 暴露; |
||||||
|
- [ ] 用户 callable 仍使用统一的 `php_` ABI 生成器; |
||||||
|
- [ ] 新名称不会与用户可声明的 PHP 函数或方法发生冲突; |
||||||
|
- [ ] bin、lib、ext 和 WASM 构建使用相同的项目名推导规则; |
||||||
|
- [ ] 修改公开 callable 映射时同步评估 stub 和既有 ABI; |
||||||
|
- [ ] 至少增加一个用户同名函数的编译回归测试。 |
||||||
|
|
||||||
|
当前相关回归测试为: |
||||||
|
|
||||||
|
```text |
||||||
|
tests/compiler/basic/helper-symbol-collision.phpt |
||||||
|
``` |
||||||
|
|
||||||
|
## 8. 主要实现位置 |
||||||
|
|
||||||
|
| 责任 | 文件 | |
||||||
|
| --- | --- | |
||||||
|
| `php_` callable 前缀与组合分隔符 | `src/CompilerBase.php` | |
||||||
|
| callable 组合冲突检测 | `src/Preprocessor.php` | |
||||||
|
| `typephp_<project>` 生成及项目私有表 | `src/Translator.php` | |
||||||
|
| TypePHP extension 前缀常量 | `src/Metadata/Constants.php` | |
||||||
|
| PHPX/TypePHP helper 分类 | `vendor/swoole/phpx/include/typephp_helper.h` | |
||||||
|
| embed module accessor 拼接 | `vendor/swoole/phpx/src/misc/typephp_main.cc` | |
||||||
@ -0,0 +1,114 @@ |
|||||||
|
# `#[Immutable]` compile-time effect checking |
||||||
|
|
||||||
|
## Purpose |
||||||
|
|
||||||
|
`#[Immutable]` is a TypePHP compile-time annotation modelled after C++ `const`. |
||||||
|
It prevents accidental mutation in statically compiled code without adding a |
||||||
|
wrapper object, Zend metadata, runtime branch, or ABI change. |
||||||
|
|
||||||
|
It is intentionally a best-effort static tool rather than a security boundary. |
||||||
|
Calls whose target is deliberately made dynamic are an escape hatch and do not |
||||||
|
receive a runtime guard. |
||||||
|
|
||||||
|
## Supported targets |
||||||
|
|
||||||
|
```php |
||||||
|
#[Immutable] |
||||||
|
public function name(): string |
||||||
|
{ |
||||||
|
return $this->name; |
||||||
|
} |
||||||
|
|
||||||
|
function inspect(#[Immutable] User $user): string |
||||||
|
{ |
||||||
|
return $user->name(); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
The attribute is valid on methods and on function, method, and closure |
||||||
|
parameters. On an instance method it makes `$this` immutable. On a parameter it |
||||||
|
makes the binding immutable and, when it can contain an object, treats the |
||||||
|
referenced object as immutable as well. |
||||||
|
|
||||||
|
## Rejected operations |
||||||
|
|
||||||
|
For an immutable root such as `$this` or `$user`, the compiler rejects: |
||||||
|
|
||||||
|
- assignment, destructuring, and array-element or object-property writes; |
||||||
|
- compound assignment, `++`, `--`, `unset()`, taking a reference, and |
||||||
|
`foreach (... as &$value)`; |
||||||
|
- a statically named method call unless the resolved method is also marked |
||||||
|
`#[Immutable]`; |
||||||
|
- a mutating value extension such as `$array->sort()`; read-only array/string |
||||||
|
methods remain available; |
||||||
|
- passing an object to a statically resolved parameter that is not itself |
||||||
|
`#[Immutable]`; |
||||||
|
- passing any immutable value to a mutable by-reference parameter, including |
||||||
|
extension functions such as `sort()`; |
||||||
|
- storing an immutable object identity in an object property, array, |
||||||
|
global/static variable, or returning/yielding it as a mutable value. |
||||||
|
|
||||||
|
An immutable by-reference parameter is supported. It acts like a C++ `const &`: |
||||||
|
the reference is accepted because the callee is checked against mutation. |
||||||
|
|
||||||
|
`#[MethodsFor]` follows the same contract. An object extension is callable on |
||||||
|
an immutable receiver only when its receiver parameter is marked |
||||||
|
`#[Immutable]`. |
||||||
|
|
||||||
|
## Aliases, closures, generators, and inheritance |
||||||
|
|
||||||
|
Local aliases of immutable objects remain immutable: |
||||||
|
|
||||||
|
```php |
||||||
|
$alias = $user; |
||||||
|
$alias->rename('new'); // compile-time error |
||||||
|
``` |
||||||
|
|
||||||
|
`clone` creates a distinct mutable object and therefore intentionally drops the |
||||||
|
annotation. Captured variables, arrow functions, closure `$this`, and Fiber |
||||||
|
generator bodies carry immutable metadata into their generated function |
||||||
|
contexts. |
||||||
|
|
||||||
|
An overriding class or interface method may strengthen an ordinary contract by |
||||||
|
adding `#[Immutable]`, but it cannot remove `#[Immutable]` from an inherited |
||||||
|
method or parameter. |
||||||
|
|
||||||
|
## Value versus object semantics |
||||||
|
|
||||||
|
Scalar values and PHP copy-on-write values can be read and copied normally. For |
||||||
|
example, `count($values)` and `$copy = $values` do not modify an immutable array. |
||||||
|
The compiler propagates immutability through an expression only when object |
||||||
|
identity is possible. |
||||||
|
|
||||||
|
## Explicit escape hatches |
||||||
|
|
||||||
|
The following intentionally bypass static method-effect checking: |
||||||
|
|
||||||
|
```php |
||||||
|
$method = 'rename'; |
||||||
|
$user->$method('new'); |
||||||
|
|
||||||
|
$callable = getRuntimeCallable(); |
||||||
|
$callable($user); |
||||||
|
``` |
||||||
|
|
||||||
|
The same applies to other runtime-only mechanisms that hide the target from the |
||||||
|
compiler, including reflection and dynamic ZendVM code. TypePHP neither inserts |
||||||
|
a runtime read-only proxy nor attempts to recover the escaped value later. |
||||||
|
|
||||||
|
This boundary is deliberate: `#[Immutable]` should cost nothing in generated |
||||||
|
code and should not complicate PHPX/ZendVM object semantics. |
||||||
|
|
||||||
|
## Property hooks and magic access |
||||||
|
|
||||||
|
Property-hook reads are lowered to generated method calls. Consequently, a hook |
||||||
|
used through an immutable receiver must itself carry an `#[Immutable]` method |
||||||
|
contract; otherwise the generated call is rejected. Fully dynamic magic access |
||||||
|
is covered by the same escape-hatch rule as other runtime-only behavior. |
||||||
|
|
||||||
|
## Implementation boundaries |
||||||
|
|
||||||
|
The implementation is isolated in `src/Immutable/ImmutableSupportTrait.php`. |
||||||
|
`FunctionDef` and `ArgInfo` retain only compile-time effect bits, while each |
||||||
|
`FunctionContext` stores the immutable roots and object aliases relevant to that |
||||||
|
body. Checks run during AST lowering and emit no C++ code when successful. |
||||||
@ -0,0 +1,116 @@ |
|||||||
|
# Interface Property Hooks 实现方案 |
||||||
|
|
||||||
|
本文记录 TP-AOT-010 的设计与实施计划。目标是支持 PHP 8.4 的 Interface Property Hook 契约,同时保持 TypePHP Native 调用的零成本抽象,并让 PHP 8.4 ZendVM 的 Reflection、动态类链接和继承检查获得完整元数据。 |
||||||
|
|
||||||
|
## 当前状态(2026-08-14) |
||||||
|
|
||||||
|
第一阶段已经落地:Interface 契约模型、AOT 实现检查、get/set 方向方差、PHPX 抽象 Hook 元数据、Reflection、动态 PHP 实现类及回归测试均已接通。显式 setter 参数类型仍按下文约定在编译期拒绝;完成独立写入类型模型后再开放。 |
||||||
|
|
||||||
|
## 1. 设计结论 |
||||||
|
|
||||||
|
Interface 中的 Hooked Property 只表示属性契约: |
||||||
|
|
||||||
|
```php |
||||||
|
interface Named |
||||||
|
{ |
||||||
|
public string $name { get; set; } |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
- Interface 不持有属性槽,不生成 getter/setter 实现,也不产生访问时的契约检查。 |
||||||
|
- TypePHP 在编译期验证已知 AOT 类是否满足属性的可见性、类型和 `get`/`set` 能力。 |
||||||
|
- PHP 8.4 目标在 MINIT 注册原生 Zend Hook 元数据,使 Reflection 和动态 PHP 类获得相同契约。 |
||||||
|
- TypePHP、PHPX 和最终目标运行时的最低版本均为 PHP 8.4,不提供旧版本降级路径。 |
||||||
|
|
||||||
|
## 2. 语法与诊断 |
||||||
|
|
||||||
|
支持三类契约: |
||||||
|
|
||||||
|
```php |
||||||
|
public string $readable { get; } |
||||||
|
public string $writable { set; } |
||||||
|
public string $readWrite { get; set; } |
||||||
|
``` |
||||||
|
|
||||||
|
Interface Property Hook 必须是 `public`、非 `static`、无默认值且 Hook 不得包含函数体。普通 Interface Property、`private`/`protected`、`readonly`、重复或未知 Hook,以及带实现体的 Hook 均在 TypePHP 编译期抛出 FatalError。错误信息应尽可能与 PHP 8.4 一致。 |
||||||
|
|
||||||
|
第一阶段只接收隐式 setter 参数: |
||||||
|
|
||||||
|
```php |
||||||
|
public string $name { set; } |
||||||
|
``` |
||||||
|
|
||||||
|
PHP 8.4 还允许 `set(string|Stringable $value)` 这类显式、可逆变的 setter 参数。该语法需要让编译期契约模型与 Zend Hook `arg_info` 同时保存独立于属性读取类型的写入类型;在这部分完成前,TypePHP 会给出明确的编译期错误,不生成可能错误的运行时元数据。 |
||||||
|
|
||||||
|
## 3. 编译器模型 |
||||||
|
|
||||||
|
Interface Property Hook 不应伪装成普通属性或 lowering 后的普通方法。为其建立独立契约模型,至少保存: |
||||||
|
|
||||||
|
- 属性名和声明节点; |
||||||
|
- 解析后的 TypePHP 类型与类类型; |
||||||
|
- 是否要求 `get`; |
||||||
|
- 是否要求 `set`; |
||||||
|
- 可见性及其他用于诊断的标志。 |
||||||
|
|
||||||
|
契约存放在 `InterfaceDef` 中。AST/预处理阶段只收集和验证声明,不为 Interface 分配属性槽,不运行具体类使用的 `PropertyHookLowering`,也不生成隐藏方法。 |
||||||
|
|
||||||
|
所有类型完成预处理后再执行契约链接:展开父 Interface 契约,然后检查实现类自身或父类提供的属性。普通 public backed property 同时满足读写契约;Hooked Property 根据实际 Hook 能力判断。get-only 类型按读取方向协变,set-only 类型按写入方向逆变,同时包含 get/set 时保持不变。 |
||||||
|
|
||||||
|
## 4. PHPX 与 Zend 元数据 |
||||||
|
|
||||||
|
现有 `typephp_register_property_hooks()` 用于具有真实 AOT getter/setter 的具体类,不能复用于抽象 Interface Hook。 |
||||||
|
|
||||||
|
PHPX 增加独立 helper: |
||||||
|
|
||||||
|
```cpp |
||||||
|
typephp_register_abstract_property_hooks( |
||||||
|
zend_class_entry *interface_ce, |
||||||
|
zend_property_info *property_info, |
||||||
|
bool readable, |
||||||
|
bool writable |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
TypePHP/PHPX 已统一要求 PHP 8.4+,因此该 helper 直接访问 PHP 8.4 ABI,并负责: |
||||||
|
|
||||||
|
- 持久化分配 `zend_property_info::hooks`; |
||||||
|
- 创建没有 handler 的 abstract `get`/`set` `zend_internal_function` 元数据; |
||||||
|
- 设置 `ZEND_ACC_PUBLIC | ZEND_ACC_ABSTRACT`、正确的参数/返回类型及 `common.prop_info`; |
||||||
|
- 更新 `num_hooked_props`,使 Zend inheritance 和 Reflection 识别该契约; |
||||||
|
- 保证所有字符串、Hook 表和函数描述具有 MINIT 级持久生命周期。 |
||||||
|
|
||||||
|
生成代码先注册 Interface,再以 `IS_UNDEF`、`ZEND_ACC_PUBLIC | ZEND_ACC_ABSTRACT | ZEND_ACC_VIRTUAL` 声明属性并挂载抽象 Hook,最后才注册和链接实现类。 |
||||||
|
|
||||||
|
## 5. PHP 版本边界 |
||||||
|
|
||||||
|
TypePHP 区分源码语言版本与链接运行时: |
||||||
|
|
||||||
|
- `--php-version` 只允许 `8.4` 或 `8.5`,用于解析语法和处理项目条件; |
||||||
|
- PHPX headers、`libphp` 与最终运行时必须为 PHP 8.4 或更高版本; |
||||||
|
- `--php-version` 与 `libphp.so` 的小版本不要求完全一致,例如使用 8.5 语法模式并链接 PHP 8.4 时,最终能否构建仍由实际使用的 Zend API 决定; |
||||||
|
- PHP 8.4 以下环境在 TypePHP/PHPX 构建入口直接拒绝。 |
||||||
|
|
||||||
|
## 6. TDD 覆盖 |
||||||
|
|
||||||
|
实现前先加入失败测试,覆盖: |
||||||
|
|
||||||
|
1. get-only、set-only、get/set Interface 契约; |
||||||
|
2. 普通 backed property、Hooked Property 和继承属性满足契约; |
||||||
|
3. 缺失属性、缺少 get/set、非 public 和类型不兼容的编译错误; |
||||||
|
4. Interface 继承、多个契约的合并与冲突; |
||||||
|
5. Reflection 的 abstract、virtual、hasHook/getHook 元数据; |
||||||
|
6. PHP 8.4 动态 PHP 类的成功与失败链接; |
||||||
|
7. O0/O3 结果一致,Interface 不生成属性槽或 Native Hook 实现; |
||||||
|
8. PHPX helper 在 NTS/ZTS 和 PHP 8.4/8.5 下的生命周期与 ABI 回归。 |
||||||
|
|
||||||
|
## 7. 实施顺序 |
||||||
|
|
||||||
|
1. 添加 TP-AOT-010 正常场景及语法错误 PHPT,确认当前失败。 |
||||||
|
2. 增加 Interface Property Contract 模型和预处理收集逻辑。 |
||||||
|
3. 实现 Interface 继承与实现类的编译期契约检查。 |
||||||
|
4. 在 PHPX 增加抽象 Hook 元数据 helper。 |
||||||
|
5. 修改 stub 生成和类注册顺序,接入 PHP 8.4 Zend 元数据。 |
||||||
|
6. 添加 Reflection、动态类链接、目标版本和生成代码测试。 |
||||||
|
7. 执行 Interface、Property Hook、Reflection 及全量编译器回归。 |
||||||
|
|
||||||
|
完成后的运行时属性访问仍直接进入实现类的普通属性或 Native Hook;Interface 契约本身只存在于编译期模型和 MINIT 元数据中,不进入请求热路径。 |
||||||
@ -0,0 +1,137 @@ |
|||||||
|
# Native Class 实现验收矩阵 |
||||||
|
|
||||||
|
> 审计日期:2026-08-17 |
||||||
|
> 本文记录 `#[Native]` 对象模型的需求、实现入口和直接验证证据。它是 |
||||||
|
> [NATIVE_CLASS_OBJECT.md](NATIVE_CLASS_OBJECT.md) 的实现验收附件,不替代语义设计文档。 |
||||||
|
|
||||||
|
## 1. 验收原则 |
||||||
|
|
||||||
|
每一项能力必须同时具有: |
||||||
|
|
||||||
|
1. 明确的语言边界; |
||||||
|
2. 可定位的编译器或 PHPX 实现; |
||||||
|
3. 正向 PHPT、负向 PHPUnit 或 PHPX C++ 单测中的直接证据。 |
||||||
|
|
||||||
|
仅有代码、仅有文档或“当前没有发现失败”均不视为完成。Native Object 没有 Zend |
||||||
|
表示,因此任何不能静态证明安全的跨边界行为都必须在生成 C++ 前拒绝。 |
||||||
|
|
||||||
|
## 2. 对象模型与代码生成 |
||||||
|
|
||||||
|
| 要求 | 实现证据 | 测试证据 | 结论 | |
||||||
|
|---|---|---|---| |
||||||
|
| `#[Native]` 只用于具名 class | `NativeClassAttributeLowering`、`NativeClassSupportTrait` | `testRejectsNativeAttributeOnInterface/Trait/Enum/AnonymousClass` | 已验证 | |
||||||
|
| 不注册 Zend class/object handlers | Native struct、descriptor 和自由函数生成路径 | `clone-and-zend-invisible.phpt`、Reflection 负向测试 | 已验证 | |
||||||
|
| 方法保持 `php_*` 自由函数 ABI | Native method/virtual thunk 生成路径 | `basic.phpt`、`chained-call.phpt` | 已验证 | |
||||||
|
| 静态可解析的 `new NativeClass()` 使用 Native Heap | `CompilerBase::parseNew()`、`php::nativeConstruct()` | `basic.phpt`、`construction-gc-roots.phpt` | 已验证 | |
||||||
|
| `new (表达式)()` 保持普通 PHP 动态实例化 | `parseNew()` 只对 `Node\\Name` 进入 Native 分支 | `testLeavesDynamicClassExpressionsToTheOrdinaryPhpPath` | 已验证 | |
||||||
|
| Native 对象本身不能充当动态 class target | `assertNotNativeObjectDynamicClassTarget()` | dynamic new/static call/class constant 负向测试 | 已验证 | |
||||||
|
| 所有不支持的用法在编译期终止 | Native 边界检查、类型兼容检查 | 131 项 `NativeClassValidationTest` | 已验证 | |
||||||
|
|
||||||
|
## 3. 属性与固定布局 |
||||||
|
|
||||||
|
| 要求 | 实现证据 | 测试证据 | 结论 | |
||||||
|
|---|---|---|---| |
||||||
|
| 所有属性必须声明类型 | Native field validation | `testRejectsUntypedProperty` | 已验证 | |
||||||
|
| bool/int/float 使用固定值字段 | Native field C++ type mapping | `basic.phpt`、`numeric-properties.phpt` | 已验证 | |
||||||
|
| string/array/object/typed object/Stream/mixed 可作为字段 | Native PHPX field mapping、写入检查 | `phpx-properties.phpt`、`stream-property.phpt`、`composite-property-types.phpt` | 已验证 | |
||||||
|
| BigInt/BigFloat/Decimal 可作为字段 | 高精度字段映射与 trace/destroy | `high-precision-properties.phpt` | 已验证 | |
||||||
|
| Native 类型字段保存裸指针,可形成循环类型 | struct 前置声明、descriptor trace | `mutual-reference-types.phpt`、`gc-cycle.phpt` | 已验证 | |
||||||
|
| 未显式初始化字段使用确定零值 | Native field initializer | `zero-values.phpt` | 已验证 | |
||||||
|
| 属性写入保持声明类型 | Native property assignment validation | composite、stream 及多项负向 PHPUnit | 已验证 | |
||||||
|
| 仅 `any` 属性允许取 PHP 引用 | Native property reference lowering | `any-property-reference.phpt` 及 mixed/fixed property 负向测试 | 已验证 | |
||||||
|
| Native 属性不支持 `unset()` | property unset validator | `testRejectsUnsetOnNativeObjectProperties` | 已验证 | |
||||||
|
| readonly 属性不支持 | Native declaration validator | `testRejectsReadonlyPropertyUntilNativeWriteStateIsImplemented` | 已验证 | |
||||||
|
| Box/Std Container 不能嵌入字段 | Native field validator | Box/Std Container property 负向测试 | 已验证 | |
||||||
|
|
||||||
|
## 4. 身份、空值与调用 ABI |
||||||
|
|
||||||
|
| 要求 | 实现证据 | 测试证据 | 结论 | |
||||||
|
|---|---|---|---| |
||||||
|
| `$a = $b` 只复制指针并共享对象身份 | Native pointer local representation | `parameter-semantics.phpt` | 已验证 | |
||||||
|
| Native 参数和返回必须显式声明具体类 | call argument/return boundary validation | untyped/mixed/interface 参数与返回负向测试 | 已验证 | |
||||||
|
| 普通 Native 参数非空,`?Class` 才可为空 | function entry/return checks | `non-null-parameter.phpt`、`nullable-signatures.phpt`、`return-nullability.phpt` | 已验证 | |
||||||
|
| Native 参数、返回和变量禁止 `&` | reference boundary validation | reference parameter/return/assignment/function/method 负向测试 | 已验证 | |
||||||
|
| Native variadic、union/intersection signature 不支持 | signature validation | variadic/union/null-union 负向测试 | 已验证 | |
||||||
|
| `unset($object)`/`$object = null` 只清当前 pointer slot | Native root slot lowering | `unset-alias.phpt` | 已验证 | |
||||||
|
| `===`/`!==` 与 `match` 使用指针身份 | Native identity lowering | `strict-identity.phpt`、`match-identity.phpt` | 已验证 | |
||||||
|
| ternary/match/coalesce 为兄弟子类选择最近公共 Native 基类 | `getCommonNativeObjectClass()`、selection pointer cast | `value-selection.phpt`、跨文件 global discovery 测试 | 已验证 | |
||||||
|
| 条件表达式检查非空指针,不调用 `toBool()` | Native condition lowering | `conditions.phpt` | 已验证 | |
||||||
|
| 松散比较、算术、位运算、增减、复合写入和 switch 禁止 | operator validators | 对应 PHPUnit 负向测试 | 已验证 | |
||||||
|
| `isset`/`empty`/`is_null`/nullsafe 保持 typed pointer | Native selection/nullsafe lowering | `isset-empty.phpt`、`is-null.phpt`、`nullsafe.phpt` | 已验证 | |
||||||
|
| 调用参数严格从左到右求值并在 safe point 精确 rooting | Native call argument materialization | `call-argument-roots.phpt`、`constructor-argument-roots.phpt` | 已验证 | |
||||||
|
|
||||||
|
## 5. 类语言能力 |
||||||
|
|
||||||
|
| 要求 | 实现证据 | 测试证据 | 结论 | |
||||||
|
|---|---|---|---| |
||||||
|
| 单继承、abstract 与有限虚分派 | Native C++ inheritance/virtual slot adapters | `abstract-method.phpt`、`polymorphic-clone.phpt`、`virtual-signature-variance.phpt` | 已验证 | |
||||||
|
| public/private/protected 在编译期检查 | Native member resolution | `method-visibility.phpt` 及不可访问方法/常量负向测试 | 已验证 | |
||||||
|
| Trait 在注入后按普通 Native member 编译 | 现有 Trait AST 注入 + Native member generation | `trait-inheritance-interface.phpt` | 已验证 | |
||||||
|
| Interface 仅作编译期契约,不能成为值表示 | interface contract validator | `internal-interface.phpt`、`interface-property-hooks.phpt` 及 interface escape 负向测试 | 已验证 | |
||||||
|
| 编译期可解析的 `instanceof` 折叠 | Native instanceof lowering | `instanceof.phpt`、dynamic instanceof 负向测试 | 已验证 | |
||||||
|
| Getter/Setter 注解生成直接调用 | annotation lowering + Native method path | `generators.phpt` | 已验证 | |
||||||
|
| Property Hook 只支持直接 get/set | Native hook lowering | `property-hooks.phpt`、`property-hook-native-object.phpt` 及间接操作负向测试 | 已验证 | |
||||||
|
| `clone` 保持动态子类、PHPX COW 和浅对象语义 | Native clone descriptor/thunk、`php::nativeClone()` | clone 系列 PHPT、`clone-phpx-fields.phpt` | 已验证 | |
||||||
|
| `__construct` 仅由 `new` 调用 | Native construction path、显式调用检查 | construction 系列 PHPT、explicit constructor 负向测试 | 已验证 | |
||||||
|
| `__destruct` 由 GC 至多执行一次,继承链 derived-to-base | Native finalizer chain | destructor/finalizer/lifecycle 系列 PHPT | 已验证 | |
||||||
|
| `__invoke` 和 `__toString` 使用确定 Native Call | Native magic method allow-list | `magic-methods.phpt` | 已验证 | |
||||||
|
| 动态魔术方法、变量属性/方法名不支持 | Native magic/dynamic access deny-list | dynamic magic、variable method/property 负向测试 | 已验证 | |
||||||
|
| `toArray/toString/toInt/toFloat/toBool/toObject` 要求实体方法、零参数和精确返回类型 | Native keyword method resolution | `keyword-conversions.phpt`、`testNativeObjectToObjectKeywordUsesDeclaredNativeMethod` 及签名负向测试 | 已验证 | |
||||||
|
| `count($obj)` 仅在实现 Countable 时特化 | Native count optimizer | `keyword-conversions.phpt`、count-without-countable 负向测试 | 已验证 | |
||||||
|
| `ArrayAccess` 直接语法映射到 Native `offset*()` 方法 | Native array access lowering | `array-access.phpt` | 已验证 | |
||||||
|
| Native `ArrayAccess` 禁止间接修改和引用 | writable-chain/reference validators | ArrayAccess compound/increment/nested/property/reference/coalesce 负向测试 | 已验证 | |
||||||
|
| Native `Iterator` foreach 映射到协议方法,保持 PHP 调用顺序 | Native foreach lowering | `iterator.phpt` | 已验证 | |
||||||
|
| `IteratorAggregate` 分流 Native Iterator 与 PHP Traversable | aggregate return-type lowering | `iterator.phpt` | 已验证 | |
||||||
|
| Native foreach 不枚举属性且禁止引用遍历 | interface/reference validators | foreach 负向 PHPUnit | 已验证 | |
||||||
|
|
||||||
|
## 6. GC 与生命周期 |
||||||
|
|
||||||
|
| 要求 | 实现证据 | 测试证据 | 结论 | |
||||||
|
|---|---|---|---| |
||||||
|
| Wren 风格精确、非移动、STW mark-sweep | `phpx/thirdparty/wren-gc`、`native_gc.cc` | PHPX `wren_gc.*` | 已验证 | |
||||||
|
| 裸指针写入无 RC、无 write barrier | Native pointer field/local codegen | 生成 C++ 审查、Native PHPT | 已验证 | |
||||||
|
| 16 MiB 初始阈值、1 MiB 下限、50% headroom | Wren GC 配置 | `wren_gc.uses_stable_native_heap_defaults` | 已验证 | |
||||||
|
| 精确 root frame 保持对象图存活 | `NativeRootFrame`、generated root slots | PHPX root tests、`gc-cycle.phpt` | 已验证 | |
||||||
|
| Fiber 非 LIFO 生命周期安全 | root frame registry | `fiber-lifetime.phpt`、`fiber-shutdown.phpt`、PHPX Fiber root tests | 已验证 | |
||||||
|
| global/static request roots 在 ZTS 下为 thread-local | generated globals/root registration | ZTS 环境下 `global-and-static.phpt`、PHPX request root tests | 已验证 | |
||||||
|
| RSHUTDOWN 清空 root 并销毁 heap | `nativeGcRequestShutdown()` | PHPX shutdown tests | 已验证 | |
||||||
|
| finalizer 可复活一次,之后不重复执行 | Wren/Native finalization state | `gc-cycle.phpt`、PHPX resurrection tests | 已验证 | |
||||||
|
| finalizer 中分配、异常和 Zend 状态安全 | finalizer queue/exception cleanup | finalizer/lifecycle PHPT、PHPX finalizer tests | 已验证 | |
||||||
|
| 构造或克隆失败不产生悬空对象,已逃逸对象保持有效 | `nativeConstruct()`、`nativeClone()` failure paths | `failed-lifecycle-escape.phpt`、`failed-clone-finalizer.phpt` | 已验证 | |
||||||
|
|
||||||
|
## 7. ZendVM 边界与容器 |
||||||
|
|
||||||
|
| 要求 | 实现证据 | 测试证据 | 结论 | |
||||||
|
|---|---|---|---| |
||||||
|
| Native Object 不能进入 PHP array/object property/mixed | escape and boundary validators | 对应 PHPUnit 负向测试 | 已验证 | |
||||||
|
| 不能传给 PHP/ZendVM 动态函数、Closure 或 constructor | call boundary validator | dynamic call、Closure、Zend constructor 负向测试 | 已验证 | |
||||||
|
| Reflection/WeakReference/serialize/json_encode 不支持 | facility-specific diagnostics | 对应 PHPUnit 负向测试 | 已验证 | |
||||||
|
| Generator 不能保存、接收或产出 Native pointer | generator boundary validator | generator 系列负向测试 | 已验证 | |
||||||
|
| 普通函数跨 Fiber suspend 的 Native local 有精确 root | root frame lifecycle | Fiber PHPT | 已验证 | |
||||||
|
| 局部 Std Container 可保存具体 Native pointer | Std Container Native value mapping/root frame | `std-containers.phpt` | 已验证 | |
||||||
|
| Native Std Container 不能逃逸为 Zend 值、static/global 或 closure capture | container escape validation | Std Container 系列负向 PHPUnit | 已验证 | |
||||||
|
| `include`/`eval` 不暴露 Native local 到 Zend symbol table | include scope filtering | `include-native-scope.phpt` | 已验证 | |
||||||
|
|
||||||
|
## 8. 项目级分析 |
||||||
|
|
||||||
|
| 要求 | 实现证据 | 测试证据 | 结论 | |
||||||
|
|---|---|---|---| |
||||||
|
| Native class 前向声明不依赖文件顺序 | declaration discovery pre-pass | `testDiscoversNativeTypesBeforeCrossFileSignaturePreprocessing` | 已验证 | |
||||||
|
| global Native slot ABI 在任一 C++ 文件生成前确定 | `NativeGlobalDiscovery`、`NativeGlobalTypeResolver` | `testDiscoversNativeGlobalSlotBeforeEarlierReaderIsConverted`,实际双文件构建 | 已验证 | |
||||||
|
| `global $slot` 与静态可解析的 `$GLOBALS[...]` 使用同一 Native root slot | literal/constant global slot lowering、request root registration | `global-and-static.phpt`、跨文件 Closure/常量 `$GLOBALS` fixture | 已验证 | |
||||||
|
| 动态 `$GLOBALS[$key]` 不得承载 Native Object | dynamic Zend boundary validation | `testRejectsNativeObjectStoredThroughDynamicGlobalsKey` | 已验证 | |
||||||
|
| global slot 固定首个 Native 类型,只允许子类或 null | global registration/type validation | `global-and-static.phpt`、global type change 负向测试 | 已验证 | |
||||||
|
| 未使用 Native Class 的项目跳过 Native global pre-pass | `discoverNativeGlobalObjects()` fast return | 源码检查、全量 PHPUnit | 已验证 | |
||||||
|
|
||||||
|
## 9. 当前验证命令 |
||||||
|
|
||||||
|
```bash |
||||||
|
./run-tests.php -j4 --compiler ./tpc tests/compiler/native-class/ |
||||||
|
vendor/bin/phpunit phpunit/src/NativeClass/NativeClassValidationTest.php |
||||||
|
/home/swoole/workspace/aot/phpx/build/bin/phpx-tests \ |
||||||
|
--gtest_filter='wren_gc.*:native_gc.*' |
||||||
|
``` |
||||||
|
|
||||||
|
本次 Iterator 专项结果为:`iterator.phpt` 1/1、Native Class PHPUnit 136/136, |
||||||
|
普通 foreach 回归 14/14。Native Class PHPT 目录现有 71 项;按当前任务约定暂未重复执行 |
||||||
|
该目录及编译器 PHPT 全量测试,留待下一轮统一回归。 |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,212 @@ |
|||||||
|
# Zend Object 创建与属性默认值初始化 |
||||||
|
|
||||||
|
本文记录 TypePHP 生成的 Zend Class 在 MINIT 和对象创建阶段的初始化职责,重点说明何时需要自定义 `create_object`、其中允许执行哪些行为,以及对象创建热路径上的性能边界。 |
||||||
|
|
||||||
|
本文只讨论注册到 ZendVM 的普通 TypePHP Class。`#[Native]` Class 使用 Native Heap 与 GC,不走本文流程。 |
||||||
|
|
||||||
|
## 1. 两个初始化阶段必须分开 |
||||||
|
|
||||||
|
TypePHP Class 的属性初始化分为两个阶段: |
||||||
|
|
||||||
|
1. `gen_stub.php` 在 MINIT 生成 `register_class_*()`,建立 `zend_class_entry`、属性元数据和默认属性表; |
||||||
|
2. 只有默认属性表无法准确表达的值,才在每次创建对象时由自定义 `create_object` 补充。 |
||||||
|
|
||||||
|
这两个阶段不能重复执行相同的属性赋值。`register_class_*()` 已写入的值会由 Zend 的 `object_properties_init()` 复制到新对象;再次调用 `zend_update_property()` 不仅没有语义价值,还会进入属性名查找、类型检查、handler 分派和引用计数路径。 |
||||||
|
|
||||||
|
## 2. gen_stub.php 负责的默认值 |
||||||
|
|
||||||
|
以下值可以准确写入 Zend Class 的默认属性表: |
||||||
|
|
||||||
|
| 源代码默认值 | 注册阶段表示 | 是否需要在 `create_object` 中再次写入 | |
||||||
|
|---|---|---| |
||||||
|
| `null` | `ZVAL_NULL` | 否 | |
||||||
|
| `bool` | `ZVAL_TRUE/FALSE` | 否 | |
||||||
|
| `int` | `ZVAL_LONG` | 否 | |
||||||
|
| `float` | `ZVAL_DOUBLE` | 否 | |
||||||
|
| `string` | 持久化 `zend_string` | 否 | |
||||||
|
| 标量常量表达式 | 编译期求值后的标量 zval | 否 | |
||||||
|
| `[]` | `ZVAL_EMPTY_ARRAY` | 否 | |
||||||
|
| 没有显式默认值的 TypePHP typed property | TypePHP 规定的零值、空字符串、空数组、`null` 或 `UNDEF` | 否 | |
||||||
|
|
||||||
|
例如: |
||||||
|
|
||||||
|
```php |
||||||
|
class Value |
||||||
|
{ |
||||||
|
private const BASE = 20; |
||||||
|
|
||||||
|
public int $id = self::BASE + 3; |
||||||
|
public string $name = 'type' . 'php'; |
||||||
|
public array $items = []; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
只要表达式能够在编译期安全求值,以上三个属性都应完全依赖 Zend Class 默认属性表。创建 `Value` 时不得再次调用 `zend_update_property()`。 |
||||||
|
|
||||||
|
## 3. 默认值何时需要运行时补充 |
||||||
|
|
||||||
|
当前 `gen_stub.php` 不能在默认属性表中准确表示以下值。 |
||||||
|
|
||||||
|
### 3.1 非空数组 |
||||||
|
|
||||||
|
非空数组默认值当前在注册函数中使用 `ZVAL_EMPTY_ARRAY` 作为占位值。每个对象必须构造独立、语义正确的数组值: |
||||||
|
|
||||||
|
```php |
||||||
|
class Request |
||||||
|
{ |
||||||
|
public array $options = ['timeout' => 10]; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
因此 `Request::$options` 需要在 `create_object` 中补充。多个对象仍遵守 PHP 数组的 copy-on-write 语义;修改一个对象的数组不得影响其他对象。 |
||||||
|
|
||||||
|
数组常量也遵守相同规则。若编译器只能确定它是数组、不能证明它为空,则保守地保留运行时初始化。 |
||||||
|
|
||||||
|
### 3.2 Enum case |
||||||
|
|
||||||
|
Enum case 是对象,不是标量常量: |
||||||
|
|
||||||
|
```php |
||||||
|
enum State |
||||||
|
{ |
||||||
|
case Ready; |
||||||
|
} |
||||||
|
|
||||||
|
class Task |
||||||
|
{ |
||||||
|
public State $state = State::Ready; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
类注册代码当前只能先生成占位值,`create_object` 再取得真正的 enum case 对象并写入属性。因此“只有非空数组才需要自定义 `create_object`”并不成立,enum case 是明确的第二类反例。 |
||||||
|
|
||||||
|
### 3.3 无法安全解析的常量表达式 |
||||||
|
|
||||||
|
若预处理阶段无法证明默认值可由 Zend 默认属性表准确表达,编译器必须保守地保留运行时初始化。优化只能删除已证明冗余的工作,不能根据表达式外形猜测其运行时类型。 |
||||||
|
|
||||||
|
## 4. handlers 与父类 allocator |
||||||
|
|
||||||
|
### 4.1 Property Hook 与非对称 set 可见性不单独触发 |
||||||
|
|
||||||
|
PHP 8.4 Property Hook、`private(set)` 和 `protected(set)` 会安装 TypePHP 自定义 object handlers,但这本身不要求覆盖 `create_object`。Zend 8.4 的 `object_properties_init()` 直接复制 class default table,不调用 read/write handler;普通 `php::stdCreateObject()` 已能正确设置最终 handlers。 |
||||||
|
|
||||||
|
只有该类同时含有非空数组、enum case 等运行时默认值时,才需要自定义创建流程。补充初始化必须绕过 setter;即使使用 `zend_std_write_property()`,PHP 8.4 也会根据 Hook 元数据调用 setter。当前生成代码因此使用编译期已知的 property offset,经 PHPX `Object::attr(offset)` 直接更新 backing slot。 |
||||||
|
|
||||||
|
### 4.2 父类自定义对象分配器 |
||||||
|
|
||||||
|
若父类来自 PHP 内置扩展,或祖先类拥有自定义对象存储布局,子类不能绕过父类的 allocator。当前类因运行时默认值确实需要自定义创建流程时,必须先调用保存的父类 `create_object`,再补充当前类的值。 |
||||||
|
|
||||||
|
TypePHP 父类已经安装自定义 allocator 时,普通子类通常直接继承它。只有子类自身也需要补充初始化时,才生成新的委派层。 |
||||||
|
|
||||||
|
## 5. 自定义 create_object 的执行流程 |
||||||
|
|
||||||
|
生成代码通过 `typephp_create_object_with_defaults()` 完成以下步骤: |
||||||
|
|
||||||
|
1. 保存类最终的 `default_object_handlers`; |
||||||
|
2. 若必须尊重父类对象布局,调用保存的父类 allocator;否则执行 `zend_objects_new()` 与 `object_properties_init()`; |
||||||
|
3. 临时把新对象切换到 Zend 标准 object handlers,确保异常路径和其他对象操作处于可控状态; |
||||||
|
4. 只执行标记为 `requiresRuntimeDefaultInit` 的属性初始化,并通过缓存的 declared-property offset 直接写 backing slot; |
||||||
|
5. 每次写入后检查 Zend 异常; |
||||||
|
6. 无论正常返回还是发生 C++ 异常,都恢复最终 handlers; |
||||||
|
7. 返回已完整初始化的 `zend_object *`。 |
||||||
|
|
||||||
|
初始化器是模板参数和编译期 lambda,不使用 `std::function`,也不会为 lambda 动态分配内存。`delegate_to_base` 是调用点确定的布尔值,优化构建中通常可被 C++ 编译器折叠。 |
||||||
|
|
||||||
|
以下行为不属于 `create_object`: |
||||||
|
|
||||||
|
- PHP `__construct()` 的函数体; |
||||||
|
- static property 默认值初始化;它在 `module_init()` 中完成; |
||||||
|
- 已由默认属性表表达的标量、`null` 和空数组赋值; |
||||||
|
- clone 后重新应用默认值;clone 应复制源对象当前状态,而不是重新创建默认状态。 |
||||||
|
|
||||||
|
## 6. 已修复的主要性能问题 |
||||||
|
|
||||||
|
旧生成逻辑只要类中存在任意显式非 static 默认值,就安装自定义 `create_object`,并在每次创建对象时重新 update 所有默认属性。这会产生两层重复成本: |
||||||
|
|
||||||
|
1. 只含 `public int $value = 0` 的普通类也绕过标准快速创建路径; |
||||||
|
2. 一个类只要含有一个非空数组,其他标量属性也会被逐个重复 update。 |
||||||
|
|
||||||
|
当前规则已经调整为: |
||||||
|
|
||||||
|
- 只有确实需要运行时补充的属性才使 `requireCtor` 生效; |
||||||
|
- 已由 `gen_stub.php` 准确注册的属性不会出现在运行时初始化 block 中; |
||||||
|
- 只有 Hook/非对称可见性而没有运行时默认值的类不再生成空的自定义 allocator; |
||||||
|
- Hook 与运行时默认值同时存在时,使用固定 property offset 更新 backing slot,不调用 setter。 |
||||||
|
|
||||||
|
在 micro benchmark 中,仅包含标量属性的 `new Foo()` 已从约 `1.8s` 降至约 `0.78s`,与同环境 ZendPHP 扣除空循环后的约 `0.83s` 接近。该数字只用于记录优化量级,不是跨机器性能承诺。 |
||||||
|
|
||||||
|
## 7. 已实现优化、剩余成本与后续方向 |
||||||
|
|
||||||
|
### 7.1 非空数组使用请求级模板与 copy-on-write |
||||||
|
|
||||||
|
不能把非空数组放进 internal class 的默认属性表,但这不等于必须为每个对象重新构建数组。当前生成器已经使用请求级默认值模板: |
||||||
|
|
||||||
|
1. 每个包含运行时数组默认值的类拥有一组 `THREAD_LOCAL php::Var` 模板和一个初始化状态,NTS 构建不引入锁; |
||||||
|
2. 第一次创建该类对象时,通过 `UNEXPECTED(!initialized)` 惰性构建该类的全部模板; |
||||||
|
3. 模板全部在局部临时值中成功构建后才提交并设置初始化标记,构造异常不会发布半初始化状态; |
||||||
|
4. 模板初始化发生在对象分配之前,失败时不会遗留一个尚未返回的对象; |
||||||
|
5. 后续创建对象时只把模板 zval 复制到目标 backing slot,即增加一次数组引用计数; |
||||||
|
6. 某个对象第一次修改该属性时,由 Zend/PHPX 的 `SEPARATE_ARRAY` 执行 copy-on-write; |
||||||
|
7. 在 `module_clean()` 中释放模板并重置初始化状态,request allocator 分配的 HashTable 不会跨越 RSHUTDOWN。 |
||||||
|
|
||||||
|
以如下默认值为例: |
||||||
|
|
||||||
|
```php |
||||||
|
class Request |
||||||
|
{ |
||||||
|
public array $options = [ |
||||||
|
'timeout' => 10, |
||||||
|
'headers' => ['Accept' => 'application/json'], |
||||||
|
]; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
若创建一万个对象但不修改 `$options`,数组及嵌套数组只构建一次;每个对象只持有共享 zval。若其中一个对象执行 `$request->options['timeout'] = 30`,只有该对象在写入时分离,其他对象和模板保持不变。嵌套数组也继续使用 Zend 原有的逐层 copy-on-write 规则。 |
||||||
|
|
||||||
|
PHP 属性默认数组不能包含引用,允许出现在常量表达式中的对象主要是不可变的 enum case,因此共享模板符合默认属性语义。PHPT 已覆盖顶层写入、嵌套写入、`unset`、引用写入和动态对象写入,确认这些路径都会正确分离。 |
||||||
|
|
||||||
|
不能简单地在 MINIT 构造持久化数组并传给 `zend_declare_typed_property()`。TypePHP 注册的是 `ZEND_INTERNAL_CLASS`,Zend 8.4 明确禁止 internal property 使用 refcounted default zval;`_object_properties_init()` 的 internal-class 快速路径也不会增加默认值引用计数。非空 array 与 enum object 都属于 refcounted value。 |
||||||
|
|
||||||
|
因此,在不改变“TypePHP Class 注册为 internal class”这一基础设计、也不修改 Zend ABI 的前提下,非空数组仍不进入 class default table;表外请求级模板把数组构造成本从“每个对象一次”降为“每个请求、每个默认值一次”。未修改默认数组的对象只承担 zval 复制和引用计数成本,实际修改的对象才承担数组分离成本。 |
||||||
|
|
||||||
|
模板按类惰性初始化,而不是在 RINIT 无条件构建全部模板:大型项目中很多类在一次请求内不会实例化。每个对象只增加一个高度可预测的初始化状态分支;第一次之后该分支稳定为 false。 |
||||||
|
|
||||||
|
暂不生成模块生命周期的 persistent immutable template。该方案需要完整验证 persistent HashTable、interned string、嵌套数组、MSHUTDOWN 和 ZTS,并且包含运行时常量或 enum case 的数组仍要走请求级路径。在 ZendVM 对这些组合的约束得到充分验证前,请求级模板是安全边界。 |
||||||
|
|
||||||
|
### 7.2 已改为固定属性槽写入 |
||||||
|
|
||||||
|
运行时补充的属性在编译期已经知道 class、属性名、offset 和类型。当前实现复用 persistent property-offset cache,并通过 `php::Object::attr(offset)` 更新槽位,已经省去每个对象上的属性名 hash 查询、通用 write handler 和 Property Hook setter。 |
||||||
|
|
||||||
|
这里仍会为 initializer 建立一个短生命周期 `php::Object` carrier,并读取 offset cache。后续若 profiling 证明它是热点,可以在 MINIT 后直接保存最终 offset,或在 PHPX 增加不取得对象所有权的初始化 helper。任何进一步优化都必须继续处理旧值析构、引用计数、父类 private slot、Hook backing slot 和异常安全,不能退回裸指针的无保护赋值。 |
||||||
|
|
||||||
|
### 7.3 Enum case 可提前绑定 |
||||||
|
|
||||||
|
Enum case 同样是 refcounted object,不能直接作为 internal class 默认 zval。可考虑在 MINIT 缓存稳定的 enum case 指针或 zval,再在每次创建对象时执行正确的引用计数复制,从而省去重复 class/case 查找;仍不能省略对象属性写入本身。 |
||||||
|
|
||||||
|
### 7.4 继承链上的多层 allocator |
||||||
|
|
||||||
|
父类和子类都拥有运行时默认值时,创建流程会逐层委派并执行各自初始化,成本随相关继承层数增长。未来可以对完全由 TypePHP 控制、且没有特殊对象布局的继承链合并初始化计划;内置扩展父类仍必须调用其 allocator。 |
||||||
|
|
||||||
|
### 7.5 保守常量可能产生不必要的 allocator |
||||||
|
|
||||||
|
无法在预处理阶段解析的常量会保守进入运行时路径。可以在符号准备完成后增加一次统一的常量默认值分类,减少“实际是标量,但早期无法证明”的自定义 allocator。该优化必须保留 enum case 和数组常量的区别。 |
||||||
|
|
||||||
|
### 7.6 自定义 handlers 的动态访问成本 |
||||||
|
|
||||||
|
TypePHP 当前为普通 Zend Class 安装属性 handlers,以支持 typed property 的 unset 语义、Property Hook 和非对称写可见性。安装发生在 MINIT,不等同于安装自定义 `create_object`;但动态属性读写仍可能进入 handler。已被编译器解析为固定槽位的 Native 属性访问不应因此退化。 |
||||||
|
|
||||||
|
## 8. 回归测试要求 |
||||||
|
|
||||||
|
修改该流程至少应覆盖: |
||||||
|
|
||||||
|
- 标量、标量常量表达式和空数组不生成自定义 allocator; |
||||||
|
- 非空数组生成 allocator,且两个对象的数组修改互不影响; |
||||||
|
- enum case 默认值在对象创建后是真正的 enum object; |
||||||
|
- 仅含 Property Hook 或非对称 set 可见性的类不生成空 allocator,且 Reflection、动态读写行为不退化; |
||||||
|
- Property Hook/非对称属性与运行时默认值组合时不触发 setter; |
||||||
|
- 父子类分别声明运行时默认值时,父类和子类属性都正确; |
||||||
|
- 继承内置扩展类时不破坏其对象布局; |
||||||
|
- 异常路径恢复 object handlers; |
||||||
|
- 自举编译和完整 PHPUnit/PHPT 回归通过。 |
||||||
|
|
||||||
|
当前针对代码生成的核心断言位于 `NewObjectCodegenTest`,运行语义由 `default-initialization-paths.phpt`、`default-expressions-inheritance.phpt` 和 Property Hook 测试组覆盖。 |
||||||
@ -0,0 +1,302 @@ |
|||||||
|
# TypePHP 三套对象存储与传递模型 |
||||||
|
|
||||||
|
> 状态:当前架构约束。本文解释 TypePHP 为什么同时保留 Zend Object、PHPX Box 和 |
||||||
|
> Native Class Object 三套对象式值模型,以及它们各自的所有权、传递方式和边界。 |
||||||
|
|
||||||
|
## 1. 结论 |
||||||
|
|
||||||
|
TypePHP 当前存在三套对象存储与传递机制: |
||||||
|
|
||||||
|
1. 普通 PHP/Zend Object; |
||||||
|
2. PHPX Box,包括 Std Container 和高精度类型; |
||||||
|
3. `#[Native]` Native Class Object。 |
||||||
|
|
||||||
|
三者并非同一设计的历史残留,而是分别解决三类互相冲突的问题: |
||||||
|
|
||||||
|
- Zend Object 保留 PHP 的动态对象语义和 ZendVM 生态兼容性; |
||||||
|
- Box 为无法完整写进 PHP 类型声明的 C++ 类型提供不透明 Zend value 载体; |
||||||
|
- Native Class Object 为静态可知的业务对象提供接近 C/C++ 的固定布局、裸指针调用和 |
||||||
|
tracing GC。 |
||||||
|
|
||||||
|
任何一种机制都不能在不损失另一种机制核心能力的前提下替代其余两种。当前设计明确接受 |
||||||
|
三套模型长期共存,不以“统一对象表示”为目标。 |
||||||
|
|
||||||
|
## 2. 总览 |
||||||
|
|
||||||
|
| 维度 | Zend Object | PHPX Box | Native Class Object | |
||||||
|
| --- | --- | --- | --- | |
||||||
|
| 典型值 | 普通 PHP class 实例 | Std Container、BigInt、BigFloat、Decimal | `#[Native] class` 实例 | |
||||||
|
| 主要表示 | `zend_object` / zval | `zend_resource` + `php::Box *` | Native Heap 中的 C++ struct + 裸指针 | |
||||||
|
| 类型身份 | `zend_class_entry *` | Box C++ 动态类型、`type_info`/类型 ID | 编译期 Native class,descriptor 保存动态类型 | |
||||||
|
| 生命周期 | Zend 引用计数 + Zend 循环 GC | Zend resource 引用计数调用 Box destructor | Wren 风格精确、非移动 mark-sweep GC | |
||||||
|
| 参数传递 | `php::Object` / `php::Var`,复制句柄并调整 RC | `php::Var` 携带 resource;热路径提取具体 C++ 引用 | 具体 `NativeClass *` 按值传递,不调整 RC | |
||||||
|
| 属性/方法访问 | Zend handlers、动态查找或已缓存 Native Call | 编译器根据具体 Box 类型生成操作 | 固定偏移字段访问和确定的 `php_*` Native Call | |
||||||
|
| 动态 PHP 互操作 | 完整 | 作为不透明 resource 有限互操作 | 不可进入 ZendVM value 边界 | |
||||||
|
| 循环图处理 | Zend GC 可扫描 Zend object graph | Zend GC 不扫描 Box 内部 C++ 对象图 | Native descriptor 精确 trace Native pointer graph | |
||||||
|
| 核心目标 | PHP 兼容性 | 携带 C++ 泛型/扩展值 | 极致静态性能 | |
||||||
|
|
||||||
|
## 3. 普通 PHP/Zend Object |
||||||
|
|
||||||
|
### 3.1 存储 |
||||||
|
|
||||||
|
普通 class 注册到 ZendVM,实例由 `zend_object` 表示。TypePHP 通过 `php::Object`、 |
||||||
|
`php::Variant`/`php::Var` 等 PHPX RAII 类型持有对应 zval。 |
||||||
|
|
||||||
|
对象具有 Zend 的 class entry、属性表、对象 handlers 和方法元数据。根据编译期信息, |
||||||
|
TypePHP 可以把部分访问优化为确定的 Native Call,但对象身份和生命周期仍属于 ZendVM。 |
||||||
|
|
||||||
|
### 3.2 传递和生命周期 |
||||||
|
|
||||||
|
PHP 对象赋值和参数传递复制对象句柄,不复制对象实体,并遵循 Zend 引用计数。对象图中的 |
||||||
|
循环引用由 Zend GC 处理。对象可以自然进入: |
||||||
|
|
||||||
|
- PHP array 和普通对象属性; |
||||||
|
- `mixed`/`object` 变量; |
||||||
|
- Closure、Generator、Fiber 和动态调用; |
||||||
|
- Reflection、序列化和扩展函数; |
||||||
|
- ZendVM 执行的 PHP 代码。 |
||||||
|
|
||||||
|
### 3.3 必须保留的原因 |
||||||
|
|
||||||
|
只有 Zend Object 能完整承载 PHP 的运行时对象语义。用 Box 替代会丢失 class entry、对象 |
||||||
|
handlers、可见性、Reflection 和动态分派;用 Native Object 替代则会失去 ZendVM 可见性, |
||||||
|
并迫使所有动态行为退化为编译期限制。 |
||||||
|
|
||||||
|
普通 PHP class 因此始终使用 Zend Object。编译器可以优化调用,但不能改变其对象模型。 |
||||||
|
|
||||||
|
## 4. PHPX Box |
||||||
|
|
||||||
|
### 4.1 存储 |
||||||
|
|
||||||
|
`php::Box` 是由 PHPX 管理的 C++ 多态基类。Box 指针注册为 Zend resource,并由 |
||||||
|
`php::Var` 携带: |
||||||
|
|
||||||
|
```text |
||||||
|
zval(IS_RESOURCE) |
||||||
|
-> zend_resource |
||||||
|
-> php::Box* |
||||||
|
-> concrete C++ value |
||||||
|
``` |
||||||
|
|
||||||
|
Zend resource 的析构回调最终调用 `Box::destroy()`。Box 因而可以经过普通 zval/Variant |
||||||
|
调用边界,同时隐藏 Zend 无法表达的具体 C++ 类型。 |
||||||
|
|
||||||
|
当前主要使用者包括: |
||||||
|
|
||||||
|
- `StdContainerBox<std::vector<T>>`; |
||||||
|
- `StdContainerBox<std::array<T, N>>`; |
||||||
|
- `StdContainerBox<map-like type>`; |
||||||
|
- BigInt、BigFloat、Decimal 等高精度值。 |
||||||
|
|
||||||
|
### 4.2 Std Container 的热路径 |
||||||
|
|
||||||
|
Std Container 局部变量具有两层表示: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Var values = php::Var(new php::StdContainerBox<Container>(type_id)); |
||||||
|
auto &values_ref = values.toBox<php::StdContainerBox<Container>>()->container; |
||||||
|
``` |
||||||
|
|
||||||
|
`php::Var` 负责生命周期和必要的边界传递,具体容器引用用于后续元素访问,避免每次操作都 |
||||||
|
重复提取 Box。容器的 key/value/长度等泛型信息由编译器和具体 C++ 模板类型共同保存。 |
||||||
|
|
||||||
|
Std Container 跨 TypePHP 函数传递时,PHP 函数签名无法表达以下 C++ 类型信息: |
||||||
|
|
||||||
|
```text |
||||||
|
std::vector<int> |
||||||
|
std::vector<string> |
||||||
|
std::map<string, App\User> |
||||||
|
``` |
||||||
|
|
||||||
|
PHP 参数最多只能声明一个非泛型类名或伪类型,不能同时携带容器种类、key 类型、value |
||||||
|
类型、数组维度和长度。当前使用 `UnsafePtr`/`std::unsafe_cast()` 加编译器类型 ID 校验, |
||||||
|
而不是把所有组合生成为 PHP class。 |
||||||
|
|
||||||
|
理论上可以增加参数和返回值注解描述泛型,但这要求每个声明、调用、返回、属性和传播点 |
||||||
|
都维护额外元数据,PHP Reflection 仍无法完整表达它。当前不引入这套独立泛型 ABI。 |
||||||
|
|
||||||
|
### 4.3 Box 的边界 |
||||||
|
|
||||||
|
Box 是不透明值载体,不是通用对象系统: |
||||||
|
|
||||||
|
- Zend GC 只看见 resource,不会扫描 Box 内部保存的 C++ 引用; |
||||||
|
- Box 不提供 PHP class 的方法表、属性表、继承和 Reflection; |
||||||
|
- 通过 `dynamic_cast`、类型 ID 或专用 helper 恢复具体类型; |
||||||
|
- 不应使用 Box 构建需要跨 Zend/Box 双向追踪的任意循环对象图; |
||||||
|
- Std Container 的可用位置和逃逸路径继续受编译器限制。 |
||||||
|
|
||||||
|
Box 适合数值、容器和其他边界明确的扩展值。它不适合代替具有任意字段引用关系的 Native |
||||||
|
业务对象。 |
||||||
|
|
||||||
|
### 4.4 必须保留的原因 |
||||||
|
|
||||||
|
Std Container 的泛型类型无法由 PHP 函数参数完整表达;高精度值又需要作为 `php::Var` |
||||||
|
参与现有运算和调用。Box 同时提供: |
||||||
|
|
||||||
|
- 可放进 zval 的稳定载体; |
||||||
|
- C++ 具体类型的运行时恢复; |
||||||
|
- Zend request 生命周期内的自动析构; |
||||||
|
- 不为每一种模板实例注册一套 PHP class 的轻量实现。 |
||||||
|
|
||||||
|
Zend Object 无法直接表达 C++ 模板实例;Native 裸指针则无法安全穿过 `php::Var` 和动态 |
||||||
|
ZendVM 边界。因此 Box 仍有独立存在的必要。 |
||||||
|
|
||||||
|
## 5. Native Class Object |
||||||
|
|
||||||
|
### 5.1 存储 |
||||||
|
|
||||||
|
`#[Native]` class 不注册 Zend class,不生成 Zend object handlers,也没有 zval 表示。每个 |
||||||
|
对象是 Native Heap 中的固定布局 C++ struct,TypePHP 局部变量、参数、返回值和字段保存 |
||||||
|
具体 Native 指针: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php_app__point *point; |
||||||
|
``` |
||||||
|
|
||||||
|
方法继续使用 TypePHP 的自由函数 ABI: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::Float php_app__point__length(php_app__point &this_); |
||||||
|
``` |
||||||
|
|
||||||
|
普通调用只传递一个指针值。不会创建 zval、注册 resource、执行引用计数或通过 |
||||||
|
`zend_call_function()`。 |
||||||
|
|
||||||
|
### 5.2 生命周期 |
||||||
|
|
||||||
|
Native Object 使用 PHPX 中独立的 Wren 风格精确、非移动、stop-the-world mark-sweep GC: |
||||||
|
|
||||||
|
- Native 局部变量、参数、返回临时值和 global/static slot 进入精确 root frame; |
||||||
|
- Native 对象 descriptor 负责 trace Native pointer 字段; |
||||||
|
- Std Container 保存 Native pointer 时注册专用 container root frame; |
||||||
|
- 循环引用由 tracing GC 回收,不依赖引用计数降为零; |
||||||
|
- 16-byte GC header 保存收集器所需的最小状态; |
||||||
|
- `__destruct()` 由 Native finalization 执行,而不是由 Zend object destructor 执行。 |
||||||
|
|
||||||
|
Native 指针赋值不增加引用计数,也不需要 write barrier。固定字段直接按 C++ 偏移访问。 |
||||||
|
|
||||||
|
### 5.3 传递边界 |
||||||
|
|
||||||
|
Native Object 参数和返回值必须显式声明具体 Native class,或受支持的 nullable 具体类型: |
||||||
|
|
||||||
|
```php |
||||||
|
function distance(Point $left, Point $right): float; |
||||||
|
function findPoint(): ?Point; |
||||||
|
``` |
||||||
|
|
||||||
|
这使编译器可以把签名直接生成为 `Point *`。Native Object 不支持: |
||||||
|
|
||||||
|
- 传给 PHP/ZendVM 函数、Closure 或动态 callable; |
||||||
|
- 保存到 PHP array、普通 Zend Object 属性或 `mixed`; |
||||||
|
- 自动转换为 `php::Object`、`php::Var` 或 Interface value; |
||||||
|
- 依靠运行时 class name 恢复类型; |
||||||
|
- 使用通用 PHPX `toObject()` helper 完成装箱或拆箱。Native Class 可以声明自己的 |
||||||
|
`toObject(): object` 方法;关键词调用会直接解析为该 Native Call,并不提供通用 bridge。 |
||||||
|
|
||||||
|
需要进入 PHP API 时,用户必须显式转换数据,例如先调用 Native `toArray(): array`,再把 |
||||||
|
结果传给 `json_encode()`。该转换产生的是数据副本,不保留 Native 对象身份。 |
||||||
|
|
||||||
|
### 5.4 必须保留的原因 |
||||||
|
|
||||||
|
Native Class 的目标是接近 C/C++ 的热路径性能: |
||||||
|
|
||||||
|
- 一个机器字的对象句柄; |
||||||
|
- 固定字段布局; |
||||||
|
- 不进行 Zend RC 增减; |
||||||
|
- 不分配 `zend_object` 或 `zend_resource` carrier; |
||||||
|
- 确定符号 Native Call; |
||||||
|
- 可由 C++ 编译器内联和去虚化。 |
||||||
|
|
||||||
|
若改用 Box,每个 Native Object 都需要 resource/zval 封装、RC 管理和具体类型恢复,而且 |
||||||
|
Zend GC 无法扫描 Box 内部 Native 指针图;这既降低性能,也不能正确替代 Native tracing |
||||||
|
GC。若改用自定义 `zend_object`,虽然能够接入 Zend GC 和动态边界,但对象 header、RC、 |
||||||
|
handlers 和访问路径都会改变 Native Class 的性能定位。 |
||||||
|
|
||||||
|
因此 Native Class 继续使用独立 Native Heap 和裸指针 ABI。 |
||||||
|
|
||||||
|
## 6. 为什么不能统一 |
||||||
|
|
||||||
|
### 6.1 不能全部改为 Zend Object |
||||||
|
|
||||||
|
这样可以统一动态语义,却会让 Std Container 泛型实例和 Native Class 都承担 Zend object |
||||||
|
header、RC、handlers、class registration 与动态访问成本。Native Class 将不再接近 C/C++, |
||||||
|
Std Container 也需要为大量模板组合设计运行时 class 体系。 |
||||||
|
|
||||||
|
### 6.2 不能全部改为 Box |
||||||
|
|
||||||
|
Box 能通过 zval 携带 C++ 值,但 Zend GC 不理解 Box 内部对象图。它不能替代普通 PHP |
||||||
|
Object 的动态元数据,也不能在保持 Native 循环回收能力的同时提供裸指针热路径。 |
||||||
|
|
||||||
|
### 6.3 不能全部改为 Native pointer |
||||||
|
|
||||||
|
Native pointer 要求完整静态类型。普通 PHP 对象需要 Reflection、动态属性、动态 callable |
||||||
|
和 Zend 扩展互操作;Std Container 的完整泛型类型又无法写入 PHP 参数签名。把这些值都 |
||||||
|
改为裸指针会产生无法静态证明安全的类型擦除,并可能导致错误指针转换和崩溃。 |
||||||
|
|
||||||
|
### 6.4 不增加自动桥接 |
||||||
|
|
||||||
|
三套模型之间不进行隐式对象身份转换。自动装箱/拆箱会隐藏分配、复制、RC 和 GC root |
||||||
|
变化,也会使编译器边界不再可靠。 |
||||||
|
|
||||||
|
允许的转换必须具有明确语义: |
||||||
|
|
||||||
|
- Std Container 转 PHP array:复制容器数据; |
||||||
|
- Native Object 的 `toArray()` 等实体方法:由用户定义并显式复制数据; |
||||||
|
- 高精度类型的显式标量转换:产生新的 PHP 标量值; |
||||||
|
- 普通 Zend Object 不会自动变成 Native Object。 |
||||||
|
|
||||||
|
## 7. 编译器实现约束 |
||||||
|
|
||||||
|
后续修改必须保持以下不变量: |
||||||
|
|
||||||
|
1. 先根据静态类型确定对象模型,再选择代码生成路径;不得在运行时猜测三者之一。 |
||||||
|
2. Native Object 不得因通用 fallback 被包装成 `php::Var` 或传入 ZendVM。 |
||||||
|
3. Box 的具体类型恢复必须校验 resource 类型和 concrete C++ 类型/类型 ID。 |
||||||
|
4. Zend Object 优化不得改变 Zend 对象身份、生命周期或动态可见性。 |
||||||
|
5. 三种模型的参数 ABI 不得混用:`php::Object`、Box-bearing `php::Var`、`NativeClass *` |
||||||
|
分别代表不同所有权和类型约束。 |
||||||
|
6. 跨模型转换必须显式,并在文档和生成代码中体现分配或复制成本。 |
||||||
|
7. 若一个新特性需要牺牲所有 Native Class 热路径来获得少量动态兼容,应优先在编译期禁止。 |
||||||
|
8. 若一种新的 C++ 泛型类型需要穿过 Zend value 边界,应优先评估 Box,而不是扩大 Native |
||||||
|
Object 的动态边界。 |
||||||
|
9. 若一个值需要完整 PHP 对象语义,应使用 Zend Object,不能把 Box 当作简化的 PHP class。 |
||||||
|
|
||||||
|
## 8. 代码位置 |
||||||
|
|
||||||
|
主要实现入口: |
||||||
|
|
||||||
|
```text |
||||||
|
普通 Zend Object |
||||||
|
compiler/src/Parser/* |
||||||
|
phpx/include/phpx.h Object / Variant / Zend API wrappers |
||||||
|
|
||||||
|
PHPX Box 与 Std Container |
||||||
|
phpx/include/phpx.h Box / StdContainerBox<T> |
||||||
|
phpx/src/core/base.cc Box resource registration and destructor |
||||||
|
compiler/src/Parser/StdContainerTrait.php |
||||||
|
|
||||||
|
Native Class Object |
||||||
|
compiler/src/NativeClass/ |
||||||
|
compiler/src/Transform/NativeClassAttributeLowering.php |
||||||
|
phpx/include/phpx_native_gc.h |
||||||
|
phpx/src/core/native_gc.cc |
||||||
|
phpx/thirdparty/wren-gc/ |
||||||
|
``` |
||||||
|
|
||||||
|
详细规则分别见 [STD_CONTAINERS.md](STD_CONTAINERS.md)、 |
||||||
|
[NATIVE_CLASS_OBJECT.md](NATIVE_CLASS_OBJECT.md) 和 |
||||||
|
[NATIVE_CLASS_IMPLEMENTATION_AUDIT.md](NATIVE_CLASS_IMPLEMENTATION_AUDIT.md)。 |
||||||
|
|
||||||
|
## 9. 当前决策 |
||||||
|
|
||||||
|
当前阶段不实施以下重构: |
||||||
|
|
||||||
|
- 不移除 Wren GC; |
||||||
|
- 不把 Native Object 改为 Box 或自定义 Zend Object; |
||||||
|
- 不给 Native Object 增加通用 `toObject()` 动态恢复机制;Native Class 自定义的 |
||||||
|
`toObject(): object` 仍是普通的确定 Native Call; |
||||||
|
- 不把 Std Container 改为无法跨签名表达类型的裸指针 ABI; |
||||||
|
- 不尝试用单一统一 wrapper 覆盖三种对象模型。 |
||||||
|
|
||||||
|
未来只有在 PHP 语言层能够稳定表达泛型参数、或者有经过 benchmark 和完整 GC 正确性验证 |
||||||
|
的新 ABI 时,才重新评估这些边界。在此之前,三套机制的共存是有意的架构选择。 |
||||||
@ -0,0 +1,355 @@ |
|||||||
|
# 重建 PHPX WASM 静态库 |
||||||
|
|
||||||
|
本文面向 TypePHP/PHPX 开发者,说明如何为 `wasm32-wasip2` 重新编译并安装 |
||||||
|
PHPX 静态库。普通 TypePHP 用户不需要执行这些步骤;发行包应直接提供完整的 |
||||||
|
WASI SDK。 |
||||||
|
|
||||||
|
## 目录约定 |
||||||
|
|
||||||
|
本文假设源码布局如下: |
||||||
|
|
||||||
|
```text |
||||||
|
/home/swoole/workspace/aot/ |
||||||
|
├── compiler/ |
||||||
|
└── phpx/ |
||||||
|
``` |
||||||
|
|
||||||
|
建议先设置 PHPX 根目录: |
||||||
|
|
||||||
|
```shell |
||||||
|
export PHPX_HOME=/home/swoole/workspace/aot/phpx |
||||||
|
``` |
||||||
|
|
||||||
|
安装前缀固定为: |
||||||
|
|
||||||
|
```text |
||||||
|
$PHPX_HOME/wasm/wasm32-wasip2 |
||||||
|
``` |
||||||
|
|
||||||
|
该目录既是已有 PHP/WASI SDK 的输入,也是 PHPX 构建结果的安装位置: |
||||||
|
|
||||||
|
```text |
||||||
|
wasm/wasm32-wasip2/ |
||||||
|
├── include/php/ PHP/WASI 头文件 |
||||||
|
├── include/phpx/ PHPX/TypePHP 运行时头文件 |
||||||
|
├── lib/libphp.a |
||||||
|
├── lib/libphpx.a |
||||||
|
├── lib/libgmp.a |
||||||
|
├── lib/libgmpxx.a |
||||||
|
├── lib/libmpfr.a |
||||||
|
├── lib/libmpdec.a |
||||||
|
├── lib/libmpdec++.a |
||||||
|
└── .typephp-wasi-sdk-abi |
||||||
|
``` |
||||||
|
|
||||||
|
不要把 host 平台的 `libphpx.so`、`phpx.dll` 或 `.a` 文件复制到这里。 |
||||||
|
WASM 静态库包含目标 ABI,不能跨 WASI、Linux、macOS 或 Windows 使用。 |
||||||
|
|
||||||
|
## 工具链准备 |
||||||
|
|
||||||
|
PHPX WASM 当前只支持 WASI 0.2 Preview 2。将 WASI SDK 加入 `PATH`: |
||||||
|
|
||||||
|
```shell |
||||||
|
export PATH=/opt/wasi-sdk-33.0/bin:$PATH |
||||||
|
``` |
||||||
|
|
||||||
|
`PATH` 只负责让 shell 和构建工具找到 WASI SDK 程序,并不会让 CMake 自动选择 |
||||||
|
WASI target。第一次配置构建目录时仍然必须传入 |
||||||
|
`-DCMAKE_TOOLCHAIN_FILE=.../wasi-sdk-p2.cmake`。如果省略它,CMake 会选择 host |
||||||
|
平台的 `/usr/bin/cc` 和 `/usr/bin/c++`,PHPX 的目标检查会立即拒绝该配置。 |
||||||
|
|
||||||
|
确认必要工具: |
||||||
|
|
||||||
|
```shell |
||||||
|
command -v wasm32-wasip2-clang |
||||||
|
command -v wasm32-wasip2-clang++ |
||||||
|
command -v llvm-ar |
||||||
|
command -v llvm-ranlib |
||||||
|
command -v llvm-nm |
||||||
|
command -v cmake |
||||||
|
command -v ninja |
||||||
|
``` |
||||||
|
|
||||||
|
确认编译目标: |
||||||
|
|
||||||
|
```shell |
||||||
|
wasm32-wasip2-clang++ --print-target-triple |
||||||
|
``` |
||||||
|
|
||||||
|
必须输出: |
||||||
|
|
||||||
|
```text |
||||||
|
wasm32-unknown-wasip2 |
||||||
|
``` |
||||||
|
|
||||||
|
安装前缀必须已经包含与当前 PHPX 匹配的 PHP/WASI 头文件和 `libphp.a`: |
||||||
|
|
||||||
|
```shell |
||||||
|
test -f "$PHPX_HOME/wasm/wasm32-wasip2/include/php/main/php.h" |
||||||
|
test -f "$PHPX_HOME/wasm/wasm32-wasip2/lib/libphp.a" |
||||||
|
``` |
||||||
|
|
||||||
|
## 日常开发:直接使用 CMake 重建 PHPX |
||||||
|
|
||||||
|
PHPX 的 `.cc` 或头文件发生变化时,直接使用 `phpx/wasm/CMakeLists.txt` 增量重建。 |
||||||
|
这是日常开发的推荐流程,不会重新下载或编译 PHP、GMP 和 MPFR,也不会重新生成 |
||||||
|
`libphp.a`。 |
||||||
|
|
||||||
|
首先从当前 WASI 编译器定位 CMake toolchain,避免依赖硬编码的 SDK 版本路径: |
||||||
|
|
||||||
|
```shell |
||||||
|
WASI_RESOURCE_DIR="$(wasm32-wasip2-clang++ --print-resource-dir)" |
||||||
|
WASI_SDK_ROOT="$(cd "$WASI_RESOURCE_DIR/../../.." && pwd)" |
||||||
|
WASI_CMAKE_TOOLCHAIN="$WASI_SDK_ROOT/share/cmake/wasi-sdk-p2.cmake" |
||||||
|
test -f "$WASI_CMAKE_TOOLCHAIN" |
||||||
|
``` |
||||||
|
|
||||||
|
### 使用 Ninja(推荐) |
||||||
|
|
||||||
|
首次配置持久化构建目录: |
||||||
|
|
||||||
|
```shell |
||||||
|
cmake \ |
||||||
|
-S "$PHPX_HOME/wasm" \ |
||||||
|
-B "$PHPX_HOME/build/wasm32-wasip2" \ |
||||||
|
-G Ninja \ |
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$WASI_CMAKE_TOOLCHAIN" \ |
||||||
|
-DCMAKE_BUILD_TYPE=Release \ |
||||||
|
-DPHPX_WASI_SDK_DIR="$PHPX_HOME/wasm/wasm32-wasip2" \ |
||||||
|
-DCMAKE_INSTALL_PREFIX="$PHPX_HOME/wasm/wasm32-wasip2" |
||||||
|
``` |
||||||
|
|
||||||
|
toolchain 在 CMake 执行 `project()` 时生效,所以只能在构建目录的第一次配置时设置。 |
||||||
|
如果该目录此前未传 toolchain、已经缓存了 host 编译器,不要直接在原缓存上补参数; |
||||||
|
改用一个新的构建目录,例如: |
||||||
|
|
||||||
|
```shell |
||||||
|
cmake \ |
||||||
|
-S "$PHPX_HOME/wasm" \ |
||||||
|
-B "$PHPX_HOME/build/wasm32-wasip2-wasi" \ |
||||||
|
-G Ninja \ |
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$WASI_CMAKE_TOOLCHAIN" \ |
||||||
|
-DCMAKE_BUILD_TYPE=Release \ |
||||||
|
-DPHPX_WASI_SDK_DIR="$PHPX_HOME/wasm/wasm32-wasip2" \ |
||||||
|
-DCMAKE_INSTALL_PREFIX="$PHPX_HOME/wasm/wasm32-wasip2" |
||||||
|
``` |
||||||
|
|
||||||
|
后续的 build/install 命令也应使用这个新目录。 |
||||||
|
|
||||||
|
编译并安装: |
||||||
|
|
||||||
|
```shell |
||||||
|
cmake --build "$PHPX_HOME/build/wasm32-wasip2" --parallel 16 |
||||||
|
cmake --install "$PHPX_HOME/build/wasm32-wasip2" |
||||||
|
``` |
||||||
|
|
||||||
|
以后 PHPX 源码再次变化时,只需要执行: |
||||||
|
|
||||||
|
```shell |
||||||
|
cmake --build "$PHPX_HOME/build/wasm32-wasip2" --parallel 16 |
||||||
|
cmake --install "$PHPX_HOME/build/wasm32-wasip2" |
||||||
|
``` |
||||||
|
|
||||||
|
CMake/Ninja 只会重新编译发生变化的源文件,然后更新安装目录中的 `libphpx.a`。 |
||||||
|
|
||||||
|
### 使用 Make |
||||||
|
|
||||||
|
可以使用 `make`,但首次配置时必须选择 `Unix Makefiles` 生成器,并使用另一个构建 |
||||||
|
目录,不能在已经由 Ninja 配置的目录中切换生成器: |
||||||
|
|
||||||
|
```shell |
||||||
|
cmake \ |
||||||
|
-S "$PHPX_HOME/wasm" \ |
||||||
|
-B "$PHPX_HOME/build/wasm32-wasip2-make" \ |
||||||
|
-G "Unix Makefiles" \ |
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$WASI_CMAKE_TOOLCHAIN" \ |
||||||
|
-DCMAKE_BUILD_TYPE=Release \ |
||||||
|
-DPHPX_WASI_SDK_DIR="$PHPX_HOME/wasm/wasm32-wasip2" \ |
||||||
|
-DCMAKE_INSTALL_PREFIX="$PHPX_HOME/wasm/wasm32-wasip2" |
||||||
|
|
||||||
|
make -C "$PHPX_HOME/build/wasm32-wasip2-make" -j16 |
||||||
|
make -C "$PHPX_HOME/build/wasm32-wasip2-make" install |
||||||
|
``` |
||||||
|
|
||||||
|
后续修改 PHPX 代码后,只需重复两条 `make` 命令。也可以使用生成器无关的形式: |
||||||
|
|
||||||
|
```shell |
||||||
|
cmake --build "$PHPX_HOME/build/wasm32-wasip2-make" --parallel 16 |
||||||
|
cmake --install "$PHPX_HOME/build/wasm32-wasip2-make" |
||||||
|
``` |
||||||
|
|
||||||
|
Ninja 与 Make 的产物相同;Ninja 通常依赖扫描和增量构建更快,因此内部开发默认 |
||||||
|
使用 Ninja。 |
||||||
|
|
||||||
|
此流程会更新: |
||||||
|
|
||||||
|
- `lib/libphpx.a` |
||||||
|
- `lib/libmpdec.a` 和 `lib/libmpdec++.a`(仅相关源码变化时重编) |
||||||
|
- `include/phpx/` 下的 PHPX 公共头文件 |
||||||
|
- `.typephp-wasi-runtime-abi` |
||||||
|
|
||||||
|
它不会更新 `libphp.a`、GMP、MPFR,也不会重写完整 SDK 的 |
||||||
|
`.typephp-wasi-sdk-abi`。因此该流程应在一个已经完整安装的 SDK 上执行。 |
||||||
|
|
||||||
|
### 强制重新编译 PHPX |
||||||
|
|
||||||
|
怀疑旧对象或 CMake 缓存不再可信时,优先使用一个新的、明确的构建目录: |
||||||
|
|
||||||
|
```shell |
||||||
|
cmake \ |
||||||
|
-S "$PHPX_HOME/wasm" \ |
||||||
|
-B "$PHPX_HOME/build/wasm32-wasip2-clean" \ |
||||||
|
-G Ninja \ |
||||||
|
-DCMAKE_TOOLCHAIN_FILE="$WASI_CMAKE_TOOLCHAIN" \ |
||||||
|
-DCMAKE_BUILD_TYPE=Release \ |
||||||
|
-DPHPX_WASI_SDK_DIR="$PHPX_HOME/wasm/wasm32-wasip2" \ |
||||||
|
-DCMAKE_INSTALL_PREFIX="$PHPX_HOME/wasm/wasm32-wasip2" |
||||||
|
|
||||||
|
cmake --build "$PHPX_HOME/build/wasm32-wasip2-clean" --parallel 16 |
||||||
|
cmake --install "$PHPX_HOME/build/wasm32-wasip2-clean" |
||||||
|
``` |
||||||
|
|
||||||
|
这样不会删除安装目录中的 `libphp.a` 和依赖库,也不会混用旧的 CMake 配置。 |
||||||
|
|
||||||
|
## 首次构建或重建 PHPX 数值依赖 |
||||||
|
|
||||||
|
以下情况使用 PHPX 的统一构建入口: |
||||||
|
|
||||||
|
- 首次建立 PHPX WASI 安装目录; |
||||||
|
- GMP 或 MPFR 版本、补丁、编译参数发生变化; |
||||||
|
- PHPX vendored mpdecimal 或其 WASI 配置发生变化; |
||||||
|
- 需要同时检查并安装 PHPX 所有 WASI 头文件和静态库。 |
||||||
|
|
||||||
|
```shell |
||||||
|
cd "$PHPX_HOME" |
||||||
|
|
||||||
|
./wasm/build.sh \ |
||||||
|
--prefix "$PHPX_HOME/wasm/wasm32-wasip2" \ |
||||||
|
--build-dir "$PHPX_HOME/build/wasm32-wasip2-sdk" \ |
||||||
|
--jobs 16 |
||||||
|
``` |
||||||
|
|
||||||
|
显式使用 `$PHPX_HOME/build/`,避免默认 `/tmp` 构建目录在重启后丢失。下载的 GMP、 |
||||||
|
MPFR 源码与构建缓存会保留,可以在后续构建中复用。 |
||||||
|
|
||||||
|
该入口会构建或安装: |
||||||
|
|
||||||
|
- `libphpx.a` |
||||||
|
- `libgmp.a`、`libgmpxx.a` |
||||||
|
- `libmpfr.a` |
||||||
|
- `libmpdec.a`、`libmpdec++.a` |
||||||
|
- 对应头文件和 PHPX runtime ABI marker |
||||||
|
|
||||||
|
它要求安装前缀中已经存在 PHP/WASI 头文件;它不会构建 `libphp.a`。 |
||||||
|
|
||||||
|
## PHP ABI 变化:重建完整 SDK |
||||||
|
|
||||||
|
如果 PHP 源码、扩展集合、PHP 配置、Zend ABI 或 PHP 安装头文件发生变化,必须从 |
||||||
|
TypePHP 编译器仓库重建完整 SDK,不能只替换 `libphpx.a`: |
||||||
|
|
||||||
|
```shell |
||||||
|
cd /home/swoole/workspace/aot/compiler |
||||||
|
|
||||||
|
./wasm/build-sdk.sh \ |
||||||
|
--prefix "$PHPX_HOME/wasm/wasm32-wasip2" \ |
||||||
|
--php-source "$PWD/projects/php-8.5.9" \ |
||||||
|
--phpx-source "$PHPX_HOME" \ |
||||||
|
--build-dir "$PWD/build/wasm-sdk" \ |
||||||
|
--jobs 16 |
||||||
|
``` |
||||||
|
|
||||||
|
完整构建依次安装 PHP 与 PHPX 部分,并在全部产物验证成功后写入: |
||||||
|
|
||||||
|
```text |
||||||
|
.typephp-wasi-sdk-abi |
||||||
|
``` |
||||||
|
|
||||||
|
不要手工伪造该 marker。marker 存在只表示构建流程声明 ABI 匹配,不能修复实际混用 |
||||||
|
的旧头文件或静态库。 |
||||||
|
|
||||||
|
## 产物检查 |
||||||
|
|
||||||
|
完成安装后检查关键文件: |
||||||
|
|
||||||
|
```shell |
||||||
|
WASI_PREFIX="$PHPX_HOME/wasm/wasm32-wasip2" |
||||||
|
|
||||||
|
test -s "$WASI_PREFIX/lib/libphpx.a" |
||||||
|
test -s "$WASI_PREFIX/lib/libphp.a" |
||||||
|
test -f "$WASI_PREFIX/include/phpx/phpx.h" |
||||||
|
test -f "$WASI_PREFIX/include/phpx/phpx_helper.h" |
||||||
|
test -f "$WASI_PREFIX/include/phpx/typephp_helper.h" |
||||||
|
|
||||||
|
llvm-ar t "$WASI_PREFIX/lib/libphpx.a" | head |
||||||
|
cat "$WASI_PREFIX/.typephp-wasi-runtime-abi" |
||||||
|
cat "$WASI_PREFIX/.typephp-wasi-sdk-abi" |
||||||
|
``` |
||||||
|
|
||||||
|
当前 marker 应分别为: |
||||||
|
|
||||||
|
```text |
||||||
|
typephp-wasip2-phpx-abi-v1 |
||||||
|
typephp-wasip2-sdk-abi-v4 |
||||||
|
``` |
||||||
|
|
||||||
|
marker 版本将随 ABI 设计升级;如果代码中的预期值已经变化,应以当前构建脚本为准, |
||||||
|
不能为了通过检测而回写旧值。 |
||||||
|
|
||||||
|
## TypePHP 回归验证 |
||||||
|
|
||||||
|
先验证 Wasmtime component: |
||||||
|
|
||||||
|
```shell |
||||||
|
cd /home/swoole/workspace/aot/compiler |
||||||
|
|
||||||
|
PHPX_HOME="$PHPX_HOME" \ |
||||||
|
./run-tests.php --wasm --compiler ./bin/tpc.php tests/wasm/ |
||||||
|
``` |
||||||
|
|
||||||
|
再验证 Wasmtime 与 Chrome 输出一致,并覆盖并行 build/output 目录隔离: |
||||||
|
|
||||||
|
```shell |
||||||
|
PHPX_HOME="$PHPX_HOME" \ |
||||||
|
./run-tests.php -j 4 --target wasm-all --compiler ./bin/tpc.php tests/wasm/ |
||||||
|
``` |
||||||
|
|
||||||
|
browser 测试还要求 `jco`、Node.js 和 Chrome 位于 `PATH`。`wasm-all` 会分别在 |
||||||
|
Wasmtime 和 Chrome 中执行每个用例,并比较两端输出。 |
||||||
|
|
||||||
|
最后构建浏览器示例: |
||||||
|
|
||||||
|
```shell |
||||||
|
cd examples/wasm-hello |
||||||
|
PHPX_HOME="$PHPX_HOME" ../../bin/tpc.php project.yml |
||||||
|
npm run build |
||||||
|
``` |
||||||
|
|
||||||
|
## 常见错误 |
||||||
|
|
||||||
|
### `PersistentCacheSlot` 或 PHPX helper 未定义 |
||||||
|
|
||||||
|
生成代码使用了新 PHPX 头文件/API,但安装前缀中的 `include/phpx/` 或 |
||||||
|
`lib/libphpx.a` 仍是旧版本。执行“日常开发:仅重建 PHPX”流程,并确保配置和安装 |
||||||
|
使用同一个 `PHPX_WASI_SDK_DIR`/`CMAKE_INSTALL_PREFIX`。 |
||||||
|
|
||||||
|
### `TypePHP WASI SDK is missing or ABI-incompatible` |
||||||
|
|
||||||
|
检查 `PHPX_HOME` 是否指向实际 PHPX 根目录,以及完整 SDK marker、PHP/PHPX 头文件 |
||||||
|
和静态库是否来自同一次兼容构建。PHP ABI 已变化时执行完整 SDK 重建。 |
||||||
|
|
||||||
|
### CMake 检测到 host 编译器 |
||||||
|
|
||||||
|
必须传入 WASI SDK 的 `wasi-sdk-p2.cmake`。不要直接用 PHPX 根目录的 host |
||||||
|
`CMakeLists.txt` 构建 WASM。将 WASI SDK 加入 `PATH` 本身并不等价于加载 CMake |
||||||
|
toolchain。如果 `CMakeCache.txt` 已记录 `/usr/bin/cc` 或 `/usr/bin/c++`,使用一个 |
||||||
|
新的构建目录重新配置。 |
||||||
|
|
||||||
|
### 修改 PHPX 后 TypePHP 仍链接旧实现 |
||||||
|
|
||||||
|
确认 `PHPX_HOME` 的优先级高于 Composer 目录,并检查实际产物时间: |
||||||
|
|
||||||
|
```shell |
||||||
|
stat "$PHPX_HOME/wasm/wasm32-wasip2/lib/libphpx.a" |
||||||
|
``` |
||||||
|
|
||||||
|
TypePHP 应从同一个 `$PHPX_HOME/wasm/wasm32-wasip2` 同时读取头文件与静态库。 |
||||||
@ -0,0 +1,223 @@ |
|||||||
|
# PHP 8.4 Property Hook 集成设计 |
||||||
|
|
||||||
|
本文记录 TypePHP 编译器与 PHPX 对 PHP 8.4 Property Hook 的实现方式,重点说明 Zend 元数据注册、对象内省、内存生命周期和版本兼容边界。本文是内部维护文档;用户侧语法说明应放在外部文档仓库。 |
||||||
|
|
||||||
|
Interface 中不带实现体的 Property Hook 属于抽象属性契约,不走本文描述的具体类 lowering 流程;其模型、方差检查和 Zend 元数据注册见 [Interface Property Hook 实现方案](INTERFACE_PROPERTY_HOOKS.md)。 |
||||||
|
|
||||||
|
## 1. 背景 |
||||||
|
|
||||||
|
TypePHP 会把 Property Hook 的函数体编译成隐藏的 AOT getter/setter。仅完成这一步,可以满足编译器明确识别出的属性读写,但 ZendVM 并不知道这些隐藏方法代表 Property Hook,因此以下动态能力会与 PHP 8.4 不一致: |
||||||
|
|
||||||
|
- `ReflectionProperty::hasHooks()`、`getHooks()` 和 `isVirtual()`; |
||||||
|
- `get_object_vars()`、`json_encode()` 和 `var_export()`; |
||||||
|
- 对象的 `foreach` 遍历; |
||||||
|
- backed property 与 virtual property 的存储差异; |
||||||
|
- ZendVM 发起的动态属性读写。 |
||||||
|
|
||||||
|
TypePHP 不单独模拟这些 PHP 行为。编译器在 lowering 后保留 Hook 元数据,类在 MINIT 阶段注册时由 PHPX 将 AOT 方法接入 PHP 8.4 原生 Property Hook 结构。此后 Reflection 和对象内省复用 ZendVM 的标准实现。 |
||||||
|
|
||||||
|
## 2. 编译流程 |
||||||
|
|
||||||
|
### 2.1 AST lowering |
||||||
|
|
||||||
|
`PropertyHookLowering` 将每个 Hook 转换为隐藏类方法,并在属性 AST 上记录: |
||||||
|
|
||||||
|
- getter/setter 对应的隐藏方法名; |
||||||
|
- Hook 是否访问自身 backing storage; |
||||||
|
- 属性是否为 virtual property。 |
||||||
|
|
||||||
|
例如: |
||||||
|
|
||||||
|
```php |
||||||
|
public string $name { |
||||||
|
get => strtoupper($this->name); |
||||||
|
set => $this->name = trim($value); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
在内部会产生等价的隐藏 getter/setter。Hook 中的 `$this->name` 会被标记为 backing access,避免再次调用 Hook 而递归。 |
||||||
|
|
||||||
|
如果 Hook 没有访问 backing storage,则该属性标记为 virtual。这个结论必须在 lowering 阶段获得,因为生成 Zend 属性声明时需要据此决定是否分配属性槽位。 |
||||||
|
|
||||||
|
### 2.2 类注册代码 |
||||||
|
|
||||||
|
`gen_stub.php` 声明属性并取得 `zend_property_info *` 后生成: |
||||||
|
|
||||||
|
```cpp |
||||||
|
typephp_register_property_hooks( |
||||||
|
class_entry, |
||||||
|
property_info, |
||||||
|
getter_method_name, |
||||||
|
setter_method_name |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
调用发生在类的持久化注册阶段,不在请求热路径中。 |
||||||
|
|
||||||
|
## 3. PHPX 注册流程 |
||||||
|
|
||||||
|
PHPX 的 `typephp_register_property_hooks()` 只在 PHP 8.4 及以上版本实现,并位于 TypePHP 专用 helper 中。 |
||||||
|
|
||||||
|
### 3.1 查找 AOT 实现方法 |
||||||
|
|
||||||
|
PHPX 从类方法表找到 lowering 生成的隐藏方法: |
||||||
|
|
||||||
|
```cpp |
||||||
|
zend_hash_str_find_ptr(&ce->function_table, method_name.data(), method_name.size()); |
||||||
|
``` |
||||||
|
|
||||||
|
该方法是已注册的 `zend_internal_function`,其 handler 最终进入 TypePHP 生成的 C++ getter/setter。查找只执行一次;属性读写时不会重复查询函数表。 |
||||||
|
|
||||||
|
### 3.2 创建 Hook 函数描述 |
||||||
|
|
||||||
|
不能直接修改或复用类方法表中的隐藏函数对象。Zend Property Hook 需要独立的函数身份和属性关联: |
||||||
|
|
||||||
|
```cpp |
||||||
|
hook->function_name = "$name::get"; // 或 "$name::set" |
||||||
|
hook->prop_info = property_info; |
||||||
|
``` |
||||||
|
|
||||||
|
PHPX 因此复制一份 `zend_internal_function` 描述,并替换 Hook 专属字段。复制不会生成另一份 C++ 实现;handler、参数信息和其他持久化数据仍来自原 AOT 方法。 |
||||||
|
|
||||||
|
独立函数描述可以避免修改隐藏方法后破坏类方法表的 key、反射名称或所有权关系,并让 Reflection 正确报告 `$name::get` 和 `$name::set`。 |
||||||
|
|
||||||
|
### 3.3 挂载属性 Hook |
||||||
|
|
||||||
|
PHP 8.4 在 `zend_property_info` 中新增了 Hook 表: |
||||||
|
|
||||||
|
```cpp |
||||||
|
property_info->hooks[ZEND_PROPERTY_HOOK_GET] = getter; |
||||||
|
property_info->hooks[ZEND_PROPERTY_HOOK_SET] = setter; |
||||||
|
``` |
||||||
|
|
||||||
|
同时必须更新: |
||||||
|
|
||||||
|
```cpp |
||||||
|
ce->num_hooked_props++; |
||||||
|
``` |
||||||
|
|
||||||
|
Zend 的 Reflection、对象属性构建和继承检查都会读取这些元数据。只注册隐藏方法而不填写 `property_info->hooks`,不会被 Zend 识别为真正的 Property Hook。 |
||||||
|
|
||||||
|
### 3.4 安装 Hook 对象遍历器 |
||||||
|
|
||||||
|
PHPX 在类没有自定义 iterator 时设置: |
||||||
|
|
||||||
|
```cpp |
||||||
|
ce->get_iterator = zend_hooked_object_get_iterator; |
||||||
|
``` |
||||||
|
|
||||||
|
`zend_hooked_object_get_iterator()` 是 PHP 8.4 在 `zend_property_hooks.h` 中导出的 `ZEND_API`。PHP 自身编译包含 Property Hook 的类时也会安装这个 iterator。 |
||||||
|
|
||||||
|
普通对象 iterator 主要遍历物理属性槽,而 Hook iterator 还负责: |
||||||
|
|
||||||
|
- 对 backed property 和 virtual property 调用 getter; |
||||||
|
- 跳过没有 getter 的 virtual property; |
||||||
|
- 执行属性可见性规则; |
||||||
|
- 拒绝不支持的引用遍历; |
||||||
|
- 合并动态属性。 |
||||||
|
|
||||||
|
因此不应在 PHPX 中复制一套遍历实现。直接复用 Zend 的导出实现可以保持 `foreach` 行为一致,并降低后续维护成本。 |
||||||
|
|
||||||
|
## 4. Virtual property |
||||||
|
|
||||||
|
PHP 8.4 使用特殊 offset 表示 virtual property: |
||||||
|
|
||||||
|
```cpp |
||||||
|
#define ZEND_VIRTUAL_PROPERTY_OFFSET ((uint32_t) -1) |
||||||
|
``` |
||||||
|
|
||||||
|
Zend 声明属性时,需要以 `IS_UNDEF` 作为声明值,才会为带 `ZEND_ACC_VIRTUAL` 的属性建立 virtual offset。因此生成代码使用: |
||||||
|
|
||||||
|
```cpp |
||||||
|
zval default_value; |
||||||
|
ZVAL_UNDEF(&default_value); |
||||||
|
``` |
||||||
|
|
||||||
|
不能用 `null` 或普通默认值代替,否则 Zend 可能分配 backing slot,`ReflectionProperty::isVirtual()` 也会得到错误结果。 |
||||||
|
|
||||||
|
## 5. 对象内省与序列化 |
||||||
|
|
||||||
|
当 `ce->num_hooked_props` 非零时,Zend 的 `zend_std_get_properties_for()` 会在 JSON、`get_object_vars()` 和 `var_export()` 等场景调用 `zend_hooked_object_build_properties()`。该函数读取 Hook 后的公开属性值。 |
||||||
|
|
||||||
|
序列化采用不同语义: |
||||||
|
|
||||||
|
- virtual property 没有持久状态,不进入序列化结果; |
||||||
|
- backed property 序列化 backing value,而不是 getter 计算后的值; |
||||||
|
- 私有存储属性仍按 PHP 的属性名修饰规则序列化。 |
||||||
|
|
||||||
|
这一区别是 PHP 8.4 的既有行为,不应为了让 JSON 和序列化输出相同而覆盖。 |
||||||
|
|
||||||
|
## 6. 生命周期与线程安全 |
||||||
|
|
||||||
|
TypePHP AOT 类以 persistent internal class 注册。Hook 表、Hook 函数描述和函数名必须具有相同的进程级生命周期,因此 PHPX 使用: |
||||||
|
|
||||||
|
```cpp |
||||||
|
pemalloc(size, true); |
||||||
|
zend_string_init(data, length, true); |
||||||
|
``` |
||||||
|
|
||||||
|
不能使用 request 内存;否则 RSHUTDOWN 后 class entry 会保留悬空指针,下一请求访问属性或 Reflection 时可能崩溃。 |
||||||
|
|
||||||
|
注册只发生在 MINIT: |
||||||
|
|
||||||
|
- 请求执行期间只读 Hook 元数据; |
||||||
|
- 不需要在每次请求重新构建; |
||||||
|
- 不需要在每次属性访问查找隐藏方法; |
||||||
|
- NTS 没有锁开销; |
||||||
|
- ZTS 下在工作线程处理请求前已完成注册,不会并发修改 class entry。 |
||||||
|
|
||||||
|
## 7. PHP 版本边界 |
||||||
|
|
||||||
|
TypePHP 与 PHPX 的最低版本均为 PHP 8.4,因此 Property Hook 实现直接使用以下 PHP 8.4 ABI: |
||||||
|
|
||||||
|
- `zend_property_info::hooks`; |
||||||
|
- `zend_class_entry::num_hooked_props`; |
||||||
|
- `ZEND_PROPERTY_HOOK_*`; |
||||||
|
- `ZEND_PROPERTY_HOOK_STRUCT_SIZE`; |
||||||
|
- `ZEND_VIRTUAL_PROPERTY_OFFSET`; |
||||||
|
- `zend_hooked_object_get_iterator()`。 |
||||||
|
|
||||||
|
PHPX 头文件和 CMake 配置会拒绝 PHP 8.4 以下的 headers/`php-config`。PHP 8.4 与 8.5 仍分别构建对应 PHPX 二进制;`--php-version` 只控制源码语法,不要求与 `libphp.so` 的小版本完全相同,但两者都必须不低于 8.4。 |
||||||
|
|
||||||
|
## 8. ABI 风险和升级检查 |
||||||
|
|
||||||
|
`zend_hooked_object_get_iterator()` 是导出的 Zend API,但 Property Hook 整体仍属于版本相关的底层 Zend ABI。PHP 8.4 没有提供一个完整的高层 `zend_declare_property_hook()` 扩展 API,因此当前实现需要填写 Zend 元数据。 |
||||||
|
|
||||||
|
采用该方案的依据是: |
||||||
|
|
||||||
|
1. TypePHP 与 PHPX 版本绑定,并针对具体 PHP 版本重新编译; |
||||||
|
2. 注册流程与 Zend 编译器处理原生 Property Hook 的步骤一致; |
||||||
|
3. 只复用 Zend 导出的 iterator,不复制其复杂实现; |
||||||
|
4. PHP 8.4 以下版本在构建入口统一拒绝; |
||||||
|
5. 所有注册均在 MINIT 完成,不增加请求热路径上的名称查找。 |
||||||
|
|
||||||
|
升级 PHP 版本时必须检查: |
||||||
|
|
||||||
|
1. `zend_property_info` 的 Hook 字段和所有权是否变化; |
||||||
|
2. `ZEND_PROPERTY_HOOK_COUNT` 和 Hook kind 是否增加; |
||||||
|
3. virtual property 的声明条件和 offset 是否变化; |
||||||
|
4. `zend_hooked_object_get_iterator()` 是否仍为导出 API; |
||||||
|
5. class linking、继承、variance 和 Reflection 是否增加新的必填元数据; |
||||||
|
6. persistent internal function 的销毁和继承复制规则是否变化。 |
||||||
|
|
||||||
|
如果 Zend 将来提供正式的扩展注册 API,应优先迁移到该 API,减少对内部结构布局的直接依赖。 |
||||||
|
|
||||||
|
## 9. 测试要求 |
||||||
|
|
||||||
|
Property Hook 改动至少需要覆盖: |
||||||
|
|
||||||
|
- 直接 getter/setter 和 backing access; |
||||||
|
- virtual property 与 backed property 的 Reflection 差异; |
||||||
|
- `hasHooks()`、`getHooks()`、Hook 名称和 final 状态; |
||||||
|
- `get_object_vars()`、JSON 和对象 `foreach`; |
||||||
|
- 序列化只包含真实存储状态; |
||||||
|
- 动态 Zend 属性读写; |
||||||
|
- 继承和属性可见性; |
||||||
|
- PHP 8.4 与 PHP 8.5 构建。 |
||||||
|
|
||||||
|
当前核心回归测试位于: |
||||||
|
|
||||||
|
- `tests/compiler/object_property/property-hooks.phpt`; |
||||||
|
- `tests/compiler/object_property/property-hooks-operations.phpt`; |
||||||
|
- `tests/compiler/object_property/property-hooks-reflection.phpt`; |
||||||
|
- `tests/compiler/object_property/property-hooks-introspection.phpt`。 |
||||||
@ -0,0 +1,151 @@ |
|||||||
|
# py2php:Python → TypePHP 源码转换工具 |
||||||
|
|
||||||
|
## 用法 |
||||||
|
|
||||||
|
```bash |
||||||
|
./bin/tpc.php --convert-python-to-php examples/python/version.py > examples/python/version.php |
||||||
|
``` |
||||||
|
|
||||||
|
生成的 PHP 源码输出到 stdout,错误输出到 stderr,退出码 0 成功 / 1 失败。 |
||||||
|
|
||||||
|
## 架构 |
||||||
|
|
||||||
|
``` |
||||||
|
.py 源码 |
||||||
|
└─ PythonAstLoader python3 子进程(ast 模块)→ JSON AST |
||||||
|
└─ PythonToTypePhpConverter AST → TypePHP 源码字符串 |
||||||
|
└─ Command::execute CLI 分发(--convert-python-to-php) |
||||||
|
``` |
||||||
|
|
||||||
|
- 源码:`src/PythonTools/Command.php`、`src/PythonTools/Converter/` |
||||||
|
- 不支持的语法抛出 `RuntimeException("{file}:{line}: unsupported Python syntax {节点类型}[: 详情]")`,CLI 层转为 stderr + 退出码 1。 |
||||||
|
- 测试:`phpunit/src/PythonTools/`(`PythonToTypePhpConverterTest`、`PythonAstLoaderTest`、`PythonToolsCommandTest`),与本文档逐项对应。 |
||||||
|
|
||||||
|
## 语句支持矩阵 |
||||||
|
|
||||||
|
| Python 语法 | 状态 | 转换规则 / 报错 | |
||||||
|
|---|---|---| |
||||||
|
| `x = expr` | ✅ | `$x = expr;`,模块级变量自动注入 `global` | |
||||||
|
| `x = y = 1`(链式赋值) | ✅ | `$x = $y = 1;`(仅限名称目标;含属性/下标目标时报错) | |
||||||
|
| `x += expr` 等增强赋值 | ✅ | 支持 `+ - * / % ** << >> \| ^ &` 系列;`//=` `@=` 展开为 `python\operator\floordiv/matmul($x, ...)` 调用 | |
||||||
|
| `x: int = expr` | ✅ | 忽略注解,转换为普通赋值 | |
||||||
|
| `x: int`(纯注解) | ✅ | 转为注释 `// annotation-only declaration: x`,不登记为模块全局 | |
||||||
|
| `a, b = x`(解构) | ✅ | `[$a, $b] = $x->toArray();`(PyObject 转 PHP 数组后解构;元素允许名称/属性/下标。嵌套解构、星号解构 `a, *b = x`、链式解构不支持。元素个数不匹配时按 PHP 语义补 null,不报 Python 的 ValueError) | |
||||||
|
| `def f(...)` | ✅ | 见「函数签名」;名为 `main` 的函数重命名为 `main_`(避免与 TypePHP 入口冲突),调用点同步改写 | |
||||||
|
| 嵌套 `def` | ❌ | `FunctionDef: nested functions require Python closure scope analysis` | |
||||||
|
| `@decorator` | ✅ | 见「函数装饰器」 | |
||||||
|
| `return [expr]` | ✅ | `return [expr];` | |
||||||
|
| `if / elif / else` | ✅ | 同构转换 | |
||||||
|
| `while` | ✅ | 同构转换;`while/else` 不支持 | |
||||||
|
| `for i in iter` | ✅ | `foreach (iter as $i)`;`for/else`、元组目标不支持 | |
||||||
|
| `break` / `continue` / `pass` | ✅ | `pass` → `// pass` 注释 | |
||||||
|
| `global x` | ✅ | `global $x;`(与自动注入的 global 并存时会重复出现,冗余但合法,属已知行为) | |
||||||
|
| `del x` / `del o.a` / `del d[k]` | ✅ | `unset(...)`;`del (a, b)` 元组/列表目标逐项展开;非法 del 目标(如 `del f()`)由 Python 解析器先行拒绝 | |
||||||
|
| 模块级字符串字面量(docstring) | ✅ | 转为 `/** ... */` 注释(`*/` 转义为 `* /`) | |
||||||
|
| `import a.b` | ✅ | `use python\a;`(仅首段作为别名,见「已知行为」) | |
||||||
|
| `import a.b as x` | ✅ | `use python\a\b as x;`(别名等于末段时省略 `as`) | |
||||||
|
| `from m import f [as g]` | ✅ | 调用点映射为 `python\m\f(...)` | |
||||||
|
| `from . import m` | ❌ | `ImportFrom: relative imports are not supported yet` | |
||||||
|
| `from m import *` | ❌ | `ImportFrom: star imports are not supported` | |
||||||
|
| `class` | ❌ | `ClassDef` | |
||||||
|
| `with` | ❌ | `With` | |
||||||
|
| `raise` / `try` / `assert` | ❌ | `Raise` / `Try` / `Assert` | |
||||||
|
| `async def` / `await` | ❌ | `AsyncFunctionDef`(`await` 不可达,外层先报错) | |
||||||
|
| `match` | ❌ | `Match` | |
||||||
|
| `nonlocal` | ❌ | `Nonlocal` | |
||||||
|
|
||||||
|
## 函数签名 |
||||||
|
|
||||||
|
| Python 形态 | 状态 | TypePHP 输出 | |
||||||
|
|---|---|---| |
||||||
|
| `def f(x, y=4)` | ✅ | `function f($x, $y = 4)` | |
||||||
|
| `def f(a, *, b)` | ✅ | `function f($a, $b = null)`(无默认值的仅关键字参数补 `null`) | |
||||||
|
| `def f(*args)` / `def f(**kw)` | ✅ | `function f(...$args)` | |
||||||
|
| `def f(*a, **kw)` | ❌ | `FunctionDef: simultaneous *args and **kwargs cannot be represented by one PHP signature` | |
||||||
|
| `lambda a, b=2: a + b` | ✅ | `fn ($a, $b = 2) => $a + $b` | |
||||||
|
|
||||||
|
## 表达式支持矩阵 |
||||||
|
|
||||||
|
| Python 语法 | 状态 | 转换规则 / 报错 | |
||||||
|
|---|---|---| |
||||||
|
| 字面量 `int / float / str / True / False / None` | ✅ | `var_export`;`None` → `null` | |
||||||
|
| `b'...'` bytes | ❌ | `{file}: Python bytes literals are not supported yet`(无行号) | |
||||||
|
| `1j` complex | ❌ | `{file}: Python complex literals are not supported yet`(无行号) | |
||||||
|
| 变量名 | ✅ | `$name`;`this` 转义为 `$this_` | |
||||||
|
| 模块别名作为值 | ❌ | `a Python module cannot be used as a first-class value in TypePHP namespace syntax` | |
||||||
|
| 属性链 `o.a.b` | ✅ | `$o->a->b`;模块别名链仅首段为模块成员:`sys.version_info.major` → `sys\version_info->major` | |
||||||
|
| 模块属性赋值/删除 | ❌ | `Attribute: Python module attributes cannot be assigned or deleted` | |
||||||
|
| 函数调用 | ✅ | 已定义函数直连 `f(...)`;内置函数映射 `python\len(...)`;`from m import f` 映射 `python\m\f(...)`;其他名字按变量可调用 `$f(...)` | |
||||||
|
| 关键字参数 / `*args` / `**kwargs` 调用 | ✅ | `f(x: 1, ...$args)` | |
||||||
|
| 容器字面量 `[] () {} {:}` | ✅ | `python\list/tuple/set/dict([...])`,支持 `...` 解包 | |
||||||
|
| 二元运算 `+ - * / % ** << >> \| ^ &` | ✅ | 同构转换 | |
||||||
|
| `//` 整除 / `@` 矩阵乘 | ✅ | `python\operator\floordiv(a, b)` / `python\operator\matmul(a, b)` | |
||||||
|
| 一元运算 `- + not ~` | ✅ | `- + ! ~` | |
||||||
|
| 比较 `== != < <= > >=` | ✅ | 同构转换 | |
||||||
|
| `is` / `is not` | ✅ | `===` / `!==` | |
||||||
|
| `in` / `not in` | ✅ | `python\operator\contains(b, a)`(参数交换)/ 取反 | |
||||||
|
| 链式比较 `a < b < c` | ❌ | `Compare: chained comparisons require explicit temporary variables` | |
||||||
|
| `a and b` / `a or b` | ❌ | `BoolOp` | |
||||||
|
| `x if c else y` | ✅ | `(c ? x : y)` | |
||||||
|
| 下标 `a[i]` / 切片 `a[l:u:s]` | ✅ | `$a[$i]` / `$a[python\slice(l, u, s)]`(缺省为 `null`) | |
||||||
|
| f-string | ✅ | 拼接 + `->toString()`;运算符等优先级敏感表达式整体加括号 | |
||||||
|
| f-string 的 `!r` 转换 / `:03d` 格式说明 | ❌ | `FormattedValue: formatted f-string conversions are not supported yet` | |
||||||
|
| 海象 `:=` | ✅ | 表达式内赋值 `($n = 10)` | |
||||||
|
| 推导式 / 生成器表达式 | ❌ | `ListComp` / `SetComp` / `DictComp` / `GeneratorExp` | |
||||||
|
| `yield` / `yield from` | ❌ | `Yield` / `YieldFrom` | |
||||||
|
|
||||||
|
## 函数装饰器 |
||||||
|
|
||||||
|
装饰器在 `main()` 起始处(其他顶层语句之前)按 Python 语义**自底向上**重绑定到同名模块变量: |
||||||
|
|
||||||
|
```python |
||||||
|
@a |
||||||
|
@b |
||||||
|
def greet(): ... |
||||||
|
``` |
||||||
|
|
||||||
|
```php |
||||||
|
function greet() { ... } |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
global $greet; |
||||||
|
$greet = b('greet'); |
||||||
|
$greet = a('greet'); |
||||||
|
... |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
- 装饰器可以是已定义函数、`from m import f` 导入符号、模块属性或装饰器工厂(`@dec('x')` → `$greet = dec('x')('greet');`) |
||||||
|
- 被装饰函数名登记为模块全局,所有调用点(包括其他函数体内)经 `global` + 变量间接调用装饰结果:`$greet()` |
||||||
|
- 被装饰函数体内的递归调用同样解析到装饰后的变量,与 Python 语义一致 |
||||||
|
|
||||||
|
## print / sys.exit 降级规则 |
||||||
|
|
||||||
|
仅当 PHP 行为与 Python 完全一致时才降级为原生语句: |
||||||
|
|
||||||
|
| 形态 | 输出 | |
||||||
|
|---|---| |
||||||
|
| `print()` | `echo "\n";` | |
||||||
|
| `print("a", "b")`(字符串/整数常量、模块属性、容器、f-string) | `echo 'a', ' ', 'b', "\n";` | |
||||||
|
| `print(1.5)`、`print(True)`、`print(x, sep=...)` | 不降级:`python\print(...)` | |
||||||
|
| 用户定义/导入/赋值遮蔽 `print` 后 | 不降级 | |
||||||
|
| `sys.exit()` / `sys.exit(2)`(含 `from sys import exit` 形式) | `exit;` / `exit(2);` | |
||||||
|
| `sys.exit("fail")` | 不降级:`sys\exit('fail');` | |
||||||
|
|
||||||
|
## 已知行为(非错误,但需留意) |
||||||
|
|
||||||
|
1. `import os.path`(无别名)只引入首段 `use python\os;`。 |
||||||
|
2. 函数内显式 `global x` 与按模块全局自动注入的 `global x` 会重复出现(合法 PHP)。 |
||||||
|
3. `print = str` 这类把内置名赋给变量的写法,右侧按变量处理(`$print = $str;`),不做内置名解析。 |
||||||
|
4. bytes/complex 字面量的报错没有行号(常量在 AST 加载阶段编码,位置信息未传递)。 |
||||||
|
5. 装饰器重绑定统一在 `main()` 起始处执行,与 Python "def 处即装饰" 的精确位置略有差异;装饰器表达式若依赖顶层语句后段的赋值,求值时机可能不同。 |
||||||
|
6. 被装饰函数名会登记为模块全局,导致所有函数的自动 `global` 注入清单中出现该名字(冗余但合法)。 |
||||||
|
|
||||||
|
## 运行测试 |
||||||
|
|
||||||
|
```bash |
||||||
|
vendor/bin/phpunit --filter 'PythonToTypePhpConverterTest|PythonAstLoaderTest|PythonToolsCommandTest' |
||||||
|
``` |
||||||
|
|
||||||
|
转换器测试依赖真实 `python3` 解析 AST,环境缺失时自动跳过。 |
||||||
@ -0,0 +1,559 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="zh-CN"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||||
|
<title>TypePHP 运行时初始化与关闭流程</title> |
||||||
|
<style> |
||||||
|
:root { |
||||||
|
color-scheme: dark; |
||||||
|
--bg: #09111f; |
||||||
|
--panel: #101b2d; |
||||||
|
--panel-2: #142238; |
||||||
|
--line: #2b3d59; |
||||||
|
--text: #e7edf7; |
||||||
|
--muted: #9baac0; |
||||||
|
--host: #94a3b8; |
||||||
|
--php: #9d8cff; |
||||||
|
--phpx: #29c7c9; |
||||||
|
--typephp: #ffae57; |
||||||
|
--project: #57d68d; |
||||||
|
--danger: #ff6b7a; |
||||||
|
--warn: #ffd166; |
||||||
|
--ok: #57d68d; |
||||||
|
--shadow: 0 18px 50px rgba(0, 0, 0, .24); |
||||||
|
} |
||||||
|
|
||||||
|
* { box-sizing: border-box; } |
||||||
|
|
||||||
|
html { scroll-behavior: smooth; } |
||||||
|
|
||||||
|
body { |
||||||
|
margin: 0; |
||||||
|
background: |
||||||
|
radial-gradient(circle at 10% 0%, rgba(157, 140, 255, .14), transparent 30rem), |
||||||
|
radial-gradient(circle at 90% 15%, rgba(41, 199, 201, .10), transparent 34rem), |
||||||
|
var(--bg); |
||||||
|
color: var(--text); |
||||||
|
font: 15px/1.7 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, |
||||||
|
"Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; |
||||||
|
} |
||||||
|
|
||||||
|
main { width: min(1240px, calc(100% - 32px)); margin: 0 auto 80px; } |
||||||
|
|
||||||
|
header { |
||||||
|
padding: 64px 0 32px; |
||||||
|
border-bottom: 1px solid var(--line); |
||||||
|
} |
||||||
|
|
||||||
|
h1, h2, h3 { line-height: 1.25; letter-spacing: -.02em; } |
||||||
|
h1 { margin: 0 0 12px; font-size: clamp(32px, 5vw, 56px); } |
||||||
|
h2 { margin: 64px 0 20px; font-size: 30px; } |
||||||
|
h3 { margin: 28px 0 12px; font-size: 20px; } |
||||||
|
p { margin: 10px 0; } |
||||||
|
a { color: #8fcbff; } |
||||||
|
|
||||||
|
code { |
||||||
|
padding: .12em .38em; |
||||||
|
border: 1px solid rgba(255, 255, 255, .08); |
||||||
|
border-radius: 5px; |
||||||
|
background: rgba(0, 0, 0, .24); |
||||||
|
color: #f2f6fc; |
||||||
|
font: .92em/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; |
||||||
|
} |
||||||
|
|
||||||
|
pre { |
||||||
|
overflow: auto; |
||||||
|
padding: 18px; |
||||||
|
border: 1px solid var(--line); |
||||||
|
border-radius: 12px; |
||||||
|
background: #07101d; |
||||||
|
box-shadow: inset 0 1px rgba(255, 255, 255, .03); |
||||||
|
} |
||||||
|
|
||||||
|
pre code { padding: 0; border: 0; background: none; } |
||||||
|
|
||||||
|
.subtitle { max-width: 920px; color: var(--muted); font-size: 18px; } |
||||||
|
.meta { margin-top: 20px; color: var(--muted); font-size: 13px; } |
||||||
|
|
||||||
|
nav { |
||||||
|
position: sticky; |
||||||
|
z-index: 20; |
||||||
|
top: 0; |
||||||
|
display: flex; |
||||||
|
gap: 8px; |
||||||
|
overflow-x: auto; |
||||||
|
padding: 12px 0; |
||||||
|
background: rgba(9, 17, 31, .88); |
||||||
|
border-bottom: 1px solid var(--line); |
||||||
|
backdrop-filter: blur(14px); |
||||||
|
} |
||||||
|
|
||||||
|
nav a { |
||||||
|
flex: none; |
||||||
|
padding: 7px 11px; |
||||||
|
border: 1px solid var(--line); |
||||||
|
border-radius: 999px; |
||||||
|
color: var(--muted); |
||||||
|
text-decoration: none; |
||||||
|
} |
||||||
|
|
||||||
|
nav a:hover { color: var(--text); border-color: #567093; } |
||||||
|
|
||||||
|
.grid { display: grid; gap: 14px; } |
||||||
|
.grid.four { grid-template-columns: repeat(4, minmax(0, 1fr)); } |
||||||
|
.grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); } |
||||||
|
.grid.two { grid-template-columns: repeat(2, minmax(0, 1fr)); } |
||||||
|
|
||||||
|
.card { |
||||||
|
padding: 18px; |
||||||
|
border: 1px solid var(--line); |
||||||
|
border-radius: 14px; |
||||||
|
background: linear-gradient(145deg, rgba(20, 34, 56, .96), rgba(13, 24, 41, .96)); |
||||||
|
box-shadow: var(--shadow); |
||||||
|
} |
||||||
|
|
||||||
|
.card h3 { margin-top: 0; } |
||||||
|
.card p:last-child { margin-bottom: 0; } |
||||||
|
.layer-host { --accent: var(--host); } |
||||||
|
.layer-php { --accent: var(--php); } |
||||||
|
.layer-phpx { --accent: var(--phpx); } |
||||||
|
.layer-typephp { --accent: var(--typephp); } |
||||||
|
.layer-project { --accent: var(--project); } |
||||||
|
.layer-card { border-top: 3px solid var(--accent); } |
||||||
|
.layer-card .tag { color: var(--accent); } |
||||||
|
|
||||||
|
.tag { |
||||||
|
display: inline-block; |
||||||
|
margin-bottom: 9px; |
||||||
|
font-weight: 750; |
||||||
|
font-size: 12px; |
||||||
|
letter-spacing: .08em; |
||||||
|
text-transform: uppercase; |
||||||
|
} |
||||||
|
|
||||||
|
.callout { |
||||||
|
margin: 20px 0; |
||||||
|
padding: 16px 18px; |
||||||
|
border-left: 4px solid var(--warn); |
||||||
|
border-radius: 0 10px 10px 0; |
||||||
|
background: rgba(255, 209, 102, .09); |
||||||
|
} |
||||||
|
|
||||||
|
.callout.danger { border-color: var(--danger); background: rgba(255, 107, 122, .09); } |
||||||
|
.callout.ok { border-color: var(--ok); background: rgba(87, 214, 141, .09); } |
||||||
|
|
||||||
|
.table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: 14px; } |
||||||
|
table { width: 100%; border-collapse: collapse; min-width: 780px; background: rgba(16, 27, 45, .78); } |
||||||
|
th, td { padding: 13px 15px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; } |
||||||
|
th { background: #17263d; color: #dce8f8; } |
||||||
|
tr:last-child td { border-bottom: 0; } |
||||||
|
td:first-child, th:first-child { white-space: nowrap; } |
||||||
|
|
||||||
|
.legend { display: flex; flex-wrap: wrap; gap: 10px 18px; margin: 18px 0; } |
||||||
|
.legend span { display: inline-flex; align-items: center; gap: 7px; color: var(--muted); } |
||||||
|
.legend i { width: 11px; height: 11px; border-radius: 3px; background: var(--accent); } |
||||||
|
|
||||||
|
.flowchart { |
||||||
|
overflow-x: auto; |
||||||
|
margin: 22px 0; |
||||||
|
padding: 20px; |
||||||
|
border: 1px solid var(--line); |
||||||
|
border-radius: 16px; |
||||||
|
background: rgba(7, 16, 29, .72); |
||||||
|
box-shadow: var(--shadow); |
||||||
|
} |
||||||
|
|
||||||
|
.flow-title { margin: 0 0 16px; font-size: 17px; } |
||||||
|
.flow { display: flex; align-items: stretch; gap: 8px; min-width: 900px; } |
||||||
|
.flow.vertical { min-width: 0; flex-direction: column; max-width: 920px; margin: 0 auto; } |
||||||
|
.flow-step { |
||||||
|
position: relative; |
||||||
|
flex: 1; |
||||||
|
min-width: 145px; |
||||||
|
padding: 13px 12px; |
||||||
|
border: 1px solid color-mix(in srgb, var(--accent), transparent 45%); |
||||||
|
border-radius: 10px; |
||||||
|
background: color-mix(in srgb, var(--accent) 11%, #101b2d); |
||||||
|
} |
||||||
|
|
||||||
|
.flow-step strong { display: block; color: var(--accent); } |
||||||
|
.flow-step small { display: block; margin-top: 5px; color: var(--muted); line-height: 1.45; } |
||||||
|
.arrow { align-self: center; flex: none; color: #69809f; font-size: 23px; font-weight: 800; } |
||||||
|
.flow.vertical .arrow { transform: rotate(90deg); } |
||||||
|
|
||||||
|
.phase { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 118px minmax(0, 1fr); |
||||||
|
gap: 14px; |
||||||
|
align-items: start; |
||||||
|
padding: 14px 0; |
||||||
|
border-bottom: 1px dashed var(--line); |
||||||
|
} |
||||||
|
|
||||||
|
.phase:last-child { border-bottom: 0; } |
||||||
|
.phase-name { color: var(--muted); font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; } |
||||||
|
.phase-steps { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; } |
||||||
|
.phase-steps .flow-step { flex: 0 1 245px; } |
||||||
|
.phase-steps .arrow { font-size: 18px; } |
||||||
|
|
||||||
|
.module-stack { display: grid; gap: 10px; } |
||||||
|
.module-row { |
||||||
|
display: grid; |
||||||
|
grid-template-columns: 150px minmax(0, 1fr) 160px; |
||||||
|
gap: 10px; |
||||||
|
align-items: center; |
||||||
|
padding: 12px; |
||||||
|
border: 1px solid var(--line); |
||||||
|
border-radius: 11px; |
||||||
|
background: rgba(20, 34, 56, .68); |
||||||
|
} |
||||||
|
|
||||||
|
.module-row .owner { color: var(--project); font-weight: 700; } |
||||||
|
.module-row .shared { color: var(--phpx); } |
||||||
|
.module-row .private { color: var(--typephp); text-align: right; } |
||||||
|
|
||||||
|
.status { font-weight: 750; } |
||||||
|
.yes { color: var(--ok); } |
||||||
|
.no { color: var(--danger); } |
||||||
|
.conditional { color: var(--warn); } |
||||||
|
|
||||||
|
ul, ol { padding-left: 22px; } |
||||||
|
li + li { margin-top: 6px; } |
||||||
|
|
||||||
|
.source-list code { word-break: break-all; } |
||||||
|
footer { margin-top: 64px; padding-top: 22px; border-top: 1px solid var(--line); color: var(--muted); } |
||||||
|
|
||||||
|
@media (max-width: 900px) { |
||||||
|
.grid.four, .grid.three, .grid.two { grid-template-columns: 1fr; } |
||||||
|
.module-row { grid-template-columns: 1fr; } |
||||||
|
.module-row .private { text-align: left; } |
||||||
|
.phase { grid-template-columns: 1fr; } |
||||||
|
} |
||||||
|
|
||||||
|
@media print { |
||||||
|
:root { color-scheme: light; --bg: #fff; --panel: #fff; --panel-2: #f7f9fc; --line: #ccd5e1; --text: #172033; --muted: #526176; } |
||||||
|
body { background: #fff; } |
||||||
|
nav { display: none; } |
||||||
|
.card, .flowchart { box-shadow: none; } |
||||||
|
} |
||||||
|
</style> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<main> |
||||||
|
<header> |
||||||
|
<h1>TypePHP 运行时生命周期</h1> |
||||||
|
<p class="subtitle">PHP、PHPX、TypePHP 生成模块与具体项目的 init/shutdown 调用关系;覆盖原生 bin、ext、lib,多 TypePHP 模块,以及 WASI command / component。</p> |
||||||
|
<p class="meta">依据当前实现整理 · 2026-08-22 · TypePHP <code>4e00799</code> · PHPX <code>f0a67ba</code> · PHP/WASI <code>4dafa96b</code></p> |
||||||
|
</header> |
||||||
|
|
||||||
|
<nav aria-label="章节导航"> |
||||||
|
<a href="#layers">四层职责</a> |
||||||
|
<a href="#matrix">模式对比</a> |
||||||
|
<a href="#ext">EXT 多模块</a> |
||||||
|
<a href="#bin">BIN + EXT</a> |
||||||
|
<a href="#lib">LIB 多模块</a> |
||||||
|
<a href="#wasm">WASM</a> |
||||||
|
<a href="#shutdown">关闭顺序</a> |
||||||
|
<a href="#names">名称辨析</a> |
||||||
|
<a href="#rules">维护规则</a> |
||||||
|
</nav> |
||||||
|
|
||||||
|
<section id="layers"> |
||||||
|
<h2>一、先分清四个层次</h2> |
||||||
|
<div class="grid four"> |
||||||
|
<article class="card layer-card layer-php"> |
||||||
|
<span class="tag">PHP / ZendVM</span> |
||||||
|
<h3>运行时所有者</h3> |
||||||
|
<p><code>php_embed_init()</code>、<code>php_request_startup()</code>、<code>php_embed_shutdown()</code>,以及 MINIT/RINIT/RSHUTDOWN/MSHUTDOWN 的最终调度者。</p> |
||||||
|
</article> |
||||||
|
<article class="card layer-card layer-phpx"> |
||||||
|
<span class="tag">PHPX</span> |
||||||
|
<h3>C++ 安全封装与共享状态</h3> |
||||||
|
<p><code>php::request_init()</code> 初始化 Decimal、Native GC、Box 资源;<code>php::request_shutdown()</code> 执行 Native finalizer 并清理请求缓存。</p> |
||||||
|
</article> |
||||||
|
<article class="card layer-card layer-typephp"> |
||||||
|
<span class="tag">TypePHP</span> |
||||||
|
<h3>生成的 Zend 模块</h3> |
||||||
|
<p>生成 <code>zend_module_entry</code>、项目专属 MINIT/MSHUTDOWN/RINIT/RSHUTDOWN,以及 <code>typephp_<project>_runtime_*</code> ABI。</p> |
||||||
|
</article> |
||||||
|
<article class="card layer-card layer-project"> |
||||||
|
<span class="tag">Project</span> |
||||||
|
<h3>项目私有状态</h3> |
||||||
|
<p>全局变量、静态属性、数组常量、请求模板、Python module cache、项目符号 cache,以及 bin 模式的 <code>main()</code>。</p> |
||||||
|
</article> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="callout"> |
||||||
|
<strong>同名但不同层:</strong>PHPX 的文件内 <code>module_init(zend_module_entry *)</code> 表示“向 Zend 注册并启动一个模块”;生成在项目命名空间里的 <code>static module_init()</code> 表示“初始化项目请求级数据”。后者不是 MINIT。 |
||||||
|
</div> |
||||||
|
|
||||||
|
<h3>生命周期嵌套关系</h3> |
||||||
|
<figure class="flowchart" aria-label="四层生命周期嵌套流程图"> |
||||||
|
<div class="flow-title">外层寿命包含内层寿命;内层必须先结束</div> |
||||||
|
<div class="flow"> |
||||||
|
<div class="flow-step layer-php"><strong>PHP / SAPI lifetime</strong><small>SINIT → module startup → module shutdown → SSHUTDOWN</small></div> |
||||||
|
<div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>TypePHP module lifetime</strong><small>MINIT → MSHUTDOWN;每个项目模块各一份</small></div> |
||||||
|
<div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-php"><strong>PHP request lifetime</strong><small>request startup → shutdown callbacks/destructors → request shutdown</small></div> |
||||||
|
<div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-project"><strong>Project request state</strong><small>RINIT/module_init → 执行 → RSHUTDOWN/module_clean</small></div> |
||||||
|
</div> |
||||||
|
</figure> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="matrix"> |
||||||
|
<h2>二、模式总览</h2> |
||||||
|
<div class="table-wrap"> |
||||||
|
<table> |
||||||
|
<thead><tr><th>模式</th><th>PHP runtime 所有者</th><th>项目模块何时注册</th><th>项目 RINIT/RSHUTDOWN</th><th>入口</th><th>多模块结论</th></tr></thead> |
||||||
|
<tbody> |
||||||
|
<tr><td><code>ext</code></td><td>外部 PHP SAPI(CLI/FPM/Embed)</td><td>PHP module startup 期间</td><td>PHP 自动调度</td><td>外部 PHP 脚本调用编译符号</td><td><span class="status yes">支持多个</span>,但 PHP 符号不得冲突</td></tr> |
||||||
|
<tr><td><code>bin</code></td><td>生成的 C++ <code>main()</code></td><td><code>php_embed_init()</code> 完成后才注册</td><td>自身项目由 PHPX 手动补调</td><td>RINIT 中 <code>php::eval(... main())</code></td><td>可再加载多个常规 ext</td></tr> |
||||||
|
<tr><td><code>lib</code></td><td>宿主显式调用项目 runtime ABI</td><td>与 bin 相同,属于晚注册模块</td><td>自身项目由 PHPX 手动补调</td><td>宿主调用导出函数,无生成 main</td><td>可加载多个库;同进程只能有一个活动 Zend runtime</td></tr> |
||||||
|
<tr><td>WASI command</td><td>WASM <code>_start/main</code></td><td>与原生 bin 同构</td><td>手动补调自身项目</td><td>执行 TypePHP <code>main()</code></td><td>单实例、静态链接;无动态 ext</td></tr> |
||||||
|
<tr><td>WASI component library</td><td>WIT runtime resource</td><td><code>create-runtime</code> 时</td><td>resource 创建/析构负责</td><td><code>#[WasmExport]</code> 方法</td><td>每个 Component 实例仅一个活动 resource</td></tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="ext"> |
||||||
|
<h2>三、EXT:PHP 自动管理多个 TypePHP 模块</h2> |
||||||
|
<p>假设 PHP 同时加载 <code>typephp_app_a</code>、<code>typephp_app_b</code>。两个模块都在 PHP 收集 request handlers 之前注册,因此走标准 Zend 模块生命周期。</p> |
||||||
|
|
||||||
|
<figure class="flowchart" aria-label="多个 TypePHP 扩展启动流程图"> |
||||||
|
<div class="flow-title">进程启动与一次 request</div> |
||||||
|
<div class="phase"> |
||||||
|
<div class="phase-name">Module startup</div> |
||||||
|
<div class="phase-steps"> |
||||||
|
<div class="flow-step layer-php"><strong>PHP module startup</strong><small>读取 extension 配置,注册模块</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>A::MINIT</strong><small>注册 A 的类、函数、属性元数据</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>B::MINIT</strong><small>注册 B;共享 hook 采用计数</small></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="phase"> |
||||||
|
<div class="phase-name">Request startup</div> |
||||||
|
<div class="phase-steps"> |
||||||
|
<div class="flow-step layer-php"><strong>php_request_startup</strong><small>zend_activate_modules()</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>A::RINIT</strong><small><code>php::request_init()</code> 首次真正初始化;A <code>module_init()</code></small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>B::RINIT</strong><small>PHPX init 幂等返回;B <code>module_init()</code></small></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="phase"> |
||||||
|
<div class="phase-name">Request shutdown</div> |
||||||
|
<div class="phase-steps"> |
||||||
|
<div class="flow-step layer-php"><strong>shutdown callbacks</strong><small>先执行注册的 shutdown function 与对象析构</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>B::RSHUTDOWN</strong><small>首次 <code>php::request_shutdown()</code> 清共享 PHPX 状态;再清 B</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>A::RSHUTDOWN</strong><small>PHPX shutdown 幂等返回;再清 A</small></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="phase"> |
||||||
|
<div class="phase-name">Module shutdown</div> |
||||||
|
<div class="phase-steps"> |
||||||
|
<div class="flow-step layer-typephp"><strong>B::MSHUTDOWN</strong><small>清 B persistent cache,共享 hook 计数减一</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>A::MSHUTDOWN</strong><small>最后一个模块恢复 Reflection handler、清 FiberGenerator 借用指针</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-php"><strong>PHP module shutdown</strong><small>Zend/SAPI 继续退出</small></div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</figure> |
||||||
|
|
||||||
|
<h3>多 ext 时哪些状态共享,哪些隔离</h3> |
||||||
|
<div class="module-stack"> |
||||||
|
<div class="module-row"><span class="owner">App A</span><span class="shared">共享:同一 PHPX request、Native GC、Box 类型、Reflection/Fiber hook</span><span class="private">私有:A globals/cache</span></div> |
||||||
|
<div class="module-row"><span class="owner">App B</span><span class="shared">共享:同一 PHPX request、Native GC、Box 类型、Reflection/Fiber hook</span><span class="private">私有:B globals/cache</span></div> |
||||||
|
<div class="module-row"><span class="owner">App C</span><span class="shared">共享:同一 PHPX request、Native GC、Box 类型、Reflection/Fiber hook</span><span class="private">私有:C globals/cache</span></div> |
||||||
|
</div> |
||||||
|
<div class="callout ok"><strong>隔离保证:</strong>生成的 C++ 数据表位于 <code>typephp_<project></code> 命名空间,runtime/get-module 符号也带项目名。多个 ext 不再因为 PHPX misc 或通用表名发生原生符号冲突。</div> |
||||||
|
<div class="callout danger"><strong>仍然禁止:</strong>两个项目向 ZendVM 注册相同的 PHP namespace/class/function 组合。C++ 符号隔离不能消除 PHP function table/class table 的语义冲突。</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="bin"> |
||||||
|
<h2>四、BIN:自身项目晚注册,额外 EXT 正常注册</h2> |
||||||
|
<p><code>php_embed_init()</code> 内部已经完成 SAPI startup、PHP module startup 和 PHP request startup。此后 bin 才取得自身的 <code>zend_module_entry</code> 并注册,所以自身项目不在 PHP 预先收集的 RINIT/RSHUTDOWN handler 列表中。</p> |
||||||
|
|
||||||
|
<figure class="flowchart" aria-label="Bin 加载额外 TypePHP 扩展的启动关闭流程图"> |
||||||
|
<div class="flow-title">bin owner + ext A + ext B</div> |
||||||
|
<div class="flow vertical"> |
||||||
|
<div class="flow-step layer-host"><strong>C++ main()</strong><small>调用 <code>typephp_<bin>_runtime_init(argc, argv)</code></small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-php"><strong>php_embed_init()</strong><small>SAPI/PHP 启动;ext A/B 自动完成 MINIT,然后在 request startup 自动完成 RINIT</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>注册 bin 自身项目模块</strong><small><code>zend_register_module_ex()</code> + <code>zend_startup_module_ex()</code> → bin::MINIT</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>手动 bin::RINIT</strong><small><code>php::request_init()</code>(通常已被 ext 初始化)→ 项目 <code>module_init()</code></small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-project"><strong>php::eval(... main())</strong><small>开始执行 TypePHP 项目;所有模块共享这一 Zend request</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-php"><strong>shutdown function + __destruct</strong><small>项目请求状态仍然存活,先让用户清理代码执行完</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>手动 bin::RSHUTDOWN</strong><small>PHPX request shutdown → bin 项目 module_clean</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>从 module_registry 删除 bin</strong><small>触发 bin::MSHUTDOWN;避免 Embed 退出时 persistent string 重复释放</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-php"><strong>php_embed_shutdown()</strong><small>PHP request shutdown → ext B/A RSHUTDOWN → ext B/A MSHUTDOWN → SAPI shutdown</small></div> |
||||||
|
</div> |
||||||
|
</figure> |
||||||
|
|
||||||
|
<div class="callout"><strong>为什么不能让 PHP 自动调用 bin 自身的 RSHUTDOWN?</strong>PHP 在 <code>php_embed_init()</code> 期间已经收集完 module handler 数组;bin 模块注册得更晚,不在数组里。不手动补调会遗漏项目全局变量、Native roots、请求数组模板和缓存清理。</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="lib"> |
||||||
|
<h2>五、LIB:生命周期由宿主显式包围</h2> |
||||||
|
<p>lib 与 bin 使用同一份 PHPX Embed 实现,但定义 <code>TYPEPHP_NO_MAIN</code>,不会生成 C++ <code>main()</code>。宿主必须调用项目名隔离的 ABI。</p> |
||||||
|
<pre><code>typephp_demo_runtime_init(argc, argv); |
||||||
|
// 调用 demo 导出的 TypePHP 函数 |
||||||
|
typephp_demo_runtime_shutdown();</code></pre> |
||||||
|
|
||||||
|
<figure class="flowchart" aria-label="Library 生命周期流程图"> |
||||||
|
<div class="flow-title">单个活动 lib runtime</div> |
||||||
|
<div class="flow"> |
||||||
|
<div class="flow-step layer-host"><strong>Host</strong><small>dlopen/LoadLibrary 只装载代码</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>runtime_init</strong><small>真正启动 PHP Embed + 项目 MINIT/RINIT</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-project"><strong>Exports</strong><small>所有调用共享一个 PHP request</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>runtime_shutdown</strong><small>项目 RSHUTDOWN/MSHUTDOWN + PHP Embed shutdown</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-host"><strong>Unload</strong><small>运行时结束后才可卸载动态库</small></div> |
||||||
|
</div> |
||||||
|
</figure> |
||||||
|
|
||||||
|
<h3>同一宿主加载多个 TypePHP lib</h3> |
||||||
|
<div class="table-wrap"> |
||||||
|
<table> |
||||||
|
<thead><tr><th>操作</th><th>结论</th><th>原因</th></tr></thead> |
||||||
|
<tbody> |
||||||
|
<tr><td>只加载 A、B 的动态库,不调用 init</td><td><span class="status yes">可以</span></td><td>导出 runtime/get-module 符号带项目名;装载代码不等于启动 ZendVM。</td></tr> |
||||||
|
<tr><td><code>init(A) → use(A) → shutdown(A) → init(B)</code></td><td><span class="status conditional">可串行</span></td><td>前一 runtime 必须完整退出后,下一库才可重新启动进程级 PHP Embed 状态。</td></tr> |
||||||
|
<tr><td><code>init(A) → init(B)</code>,两个同时活动</td><td><span class="status no">不支持</span></td><td>ZendVM/Embed 是进程级状态;每个 lib 的局部 <code>runtime_started</code> 无法阻止另一 lib 再次调用 <code>php_embed_init()</code>。</td></tr> |
||||||
|
<tr><td>多个项目需要同时工作</td><td><span class="status conditional">一个 owner</span></td><td>使用一个 bin/lib 拥有 runtime,其余能力作为启动前加载的 ext,或合并为同一个 TypePHP 项目。</td></tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
<div class="callout danger"><strong>关键约束:</strong>项目名隔离解决的是 C++ ABI 符号冲突,不会把一个进程切成多个 ZendVM。多个 native lib 的活动区间不得重叠;NTS 下也不得并发或重入调用同一 runtime。</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="wasm"> |
||||||
|
<h2>六、WASM:相同内核,两种 Host 入口</h2> |
||||||
|
<p>WASI 使用静态链接的 PHP、PHPX 和 TypePHP 生成代码。浏览器与 Wasmtime 的 Host API 不同,但 PHP/ZendVM 生命周期相同。当前不支持在运行时动态加载 <code>.so/.dll</code> 形式的 TypePHP ext。</p> |
||||||
|
<div class="callout"><strong>两个维度不要混淆:</strong><code>mode: command/library</code> 决定生命周期入口(生成 main,或由 runtime resource 管理);<code>wasm: component/browser</code> 决定产物与 Host 适配方式。它们是正交配置,浏览器构建并不自动等于 library 模式。</div> |
||||||
|
|
||||||
|
<div class="grid two"> |
||||||
|
<article class="card"> |
||||||
|
<span class="tag" style="color: var(--typephp)">WASI command</span> |
||||||
|
<h3>由 <code>_start/main</code> 自动包围</h3> |
||||||
|
<div class="flow vertical"> |
||||||
|
<div class="flow-step layer-host"><strong>Wasmtime / WASI Host</strong><small>实例化 command 并调用 <code>_start</code></small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>runtime_init</strong><small>WASI Embed + 项目 MINIT/RINIT</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-project"><strong>main()</strong><small>与原生 bin 相同的入口语义</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>runtime_shutdown</strong><small>完整执行请求、模块与 SAPI 关闭</small></div> |
||||||
|
</div> |
||||||
|
</article> |
||||||
|
<article class="card"> |
||||||
|
<span class="tag" style="color: var(--phpx)">WASI component library</span> |
||||||
|
<h3>由 WIT resource 包围</h3> |
||||||
|
<div class="flow vertical"> |
||||||
|
<div class="flow-step layer-host"><strong>Instantiate component</strong><small>只建立 WASM 实例,尚未启动 ZendVM</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>create-runtime</strong><small>调用项目 <code>runtime_init</code>,返回 resource</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-project"><strong>#[WasmExport]</strong><small>多次调用共享同一 request;异常映射为 WIT result</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>drop/dispose resource</strong><small>调用项目 <code>runtime_shutdown</code></small></div> |
||||||
|
</div> |
||||||
|
</article> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h3>WASM 多实例与多模块</h3> |
||||||
|
<ul> |
||||||
|
<li><strong>同一个 Component 实例:</strong>生成的 adapter 只允许一个活动 runtime resource,并拒绝同一 resource 上的并发或重入调用。</li> |
||||||
|
<li><strong>多个 Component 实例:</strong>各自拥有独立 linear memory 和 C/C++ 全局区,因此 ZendVM 状态互相隔离,可以由 Host 分别管理。</li> |
||||||
|
<li><strong>同一个 WASM 内的扩展:</strong>PHP 扩展必须在 SDK/最终链接阶段静态纳入,不能像原生 PHP 一样在运行时加载多个 TypePHP 动态扩展。</li> |
||||||
|
<li><strong>浏览器:</strong>应在 <code>finally</code> 中显式调用 <code>runtime[Symbol.dispose]()</code>;直接终止 Worker 只会回收整个实例,不保证 shutdown callback 被执行。</li> |
||||||
|
<li><strong>Wasmtime:</strong>command 返回时自动关闭;component library 应显式 drop resource。两者与浏览器使用相同的 TypePHP runtime ABI。</li> |
||||||
|
</ul> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="shutdown"> |
||||||
|
<h2>七、为什么关闭顺序不能随意调整</h2> |
||||||
|
<figure class="flowchart" aria-label="TypePHP 关闭顺序与资源依赖流程图"> |
||||||
|
<div class="flow-title">从仍可执行用户代码,到最终释放 PHP 内存池</div> |
||||||
|
<div class="flow"> |
||||||
|
<div class="flow-step layer-php"><strong>shutdown functions</strong><small>允许访问项目 globals</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-php"><strong>__destruct()</strong><small>允许回调 TypePHP 方法</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-phpx"><strong>PHPX request shutdown</strong><small>Native GC finalizer 仍可访问项目状态</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-project"><strong>project module_clean</strong><small>释放 globals、数组模板和请求 cache</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-typephp"><strong>MSHUTDOWN</strong><small>清 persistent pointer cache / shared hook</small></div><div class="arrow">→</div> |
||||||
|
<div class="flow-step layer-php"><strong>PHP memory manager</strong><small>最后销毁 request arena 与 interned strings</small></div> |
||||||
|
</div> |
||||||
|
</figure> |
||||||
|
|
||||||
|
<ol> |
||||||
|
<li><strong>用户清理代码在前:</strong>shutdown callback 和对象析构可能继续访问 TypePHP 全局变量、调用动态函数,不能在它们之前销毁项目状态。</li> |
||||||
|
<li><strong>PHPX 在项目 clean 之前:</strong>Native finalizer 属于用户代码,可能访问项目 globals;因此生成的 RSHUTDOWN 当前是 <code>php::request_shutdown()</code> 后接 <code>module_clean()</code>。</li> |
||||||
|
<li><strong>请求对象在 PHP 内存池之前释放:</strong>若 PHP request arena 已销毁,栈上或全局的 PHPX wrapper 再析构会变成悬空指针访问。</li> |
||||||
|
<li><strong>Embed 晚注册模块要提前移出 registry:</strong>删除操作会通过 Zend 的 module destructor 执行项目 MSHUTDOWN,同时规避 Embed 对 persistent strings 的重复释放问题。</li> |
||||||
|
<li><strong>多 ext 共享 PHPX:</strong>第一个进入的 TypePHP RSHUTDOWN 完成共享 PHPX cleanup,后续调用幂等返回;每个项目自己的 <code>module_clean()</code> 仍各执行一次。</li> |
||||||
|
</ol> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="names"> |
||||||
|
<h2>八、函数名称辨析</h2> |
||||||
|
<div class="table-wrap"> |
||||||
|
<table> |
||||||
|
<thead><tr><th>符号</th><th>定义层</th><th>寿命</th><th>职责</th></tr></thead> |
||||||
|
<tbody> |
||||||
|
<tr><td><code>php_embed_init/shutdown</code></td><td>PHP Embed SAPI</td><td>整个 runtime</td><td>启动/关闭 SAPI、PHP modules 和一个 PHP request。</td></tr> |
||||||
|
<tr><td><code>PHP_MINIT_FUNCTION(typephp_x)</code></td><td>TypePHP 生成模块</td><td>module</td><td>注册项目类/函数/属性元数据;安装共享 TypePHP handler。</td></tr> |
||||||
|
<tr><td><code>PHP_RINIT_FUNCTION(typephp_x)</code></td><td>TypePHP 生成模块</td><td>request</td><td>调用 PHPX request init,再初始化项目请求状态;bin 额外执行 main。</td></tr> |
||||||
|
<tr><td><code>php::request_init/shutdown</code></td><td>PHPX</td><td>每线程/每 request 共享</td><td>管理 Decimal、Native GC、Box 和动态调用缓存;对多模块调用幂等。</td></tr> |
||||||
|
<tr><td><code>static module_init()</code></td><td>生成的项目命名空间</td><td>request</td><td>初始化项目 globals、静态属性、数组常量、请求模板和 roots。</td></tr> |
||||||
|
<tr><td><code>static module_clean()</code></td><td>生成的项目命名空间</td><td>request</td><td>清项目 globals/cache,并将 user-code symbol pointer 表归零。</td></tr> |
||||||
|
<tr><td><code>module_init(zend_module_entry *)</code></td><td>PHPX Embed glue</td><td>module</td><td>调用 Zend API 注册晚到的 bin/lib 项目模块并触发其 MINIT。</td></tr> |
||||||
|
<tr><td><code>typephp_<project>_runtime_init/shutdown</code></td><td>TypePHP 稳定 ABI,PHPX 实现</td><td>runtime</td><td>bin main、native lib host、WIT adapter 共同使用的外层入口。</td></tr> |
||||||
|
<tr><td><code>php_<project>_embed_get_module</code></td><td>TypePHP 生成模块</td><td>module</td><td>返回该项目唯一的 <code>zend_module_entry</code>;项目名避免多模块 C++ 符号冲突。</td></tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="rules"> |
||||||
|
<h2>九、实现与维护规则</h2> |
||||||
|
<div class="grid two"> |
||||||
|
<article class="card"> |
||||||
|
<h3>必须保持</h3> |
||||||
|
<ul> |
||||||
|
<li>每个项目的 MINIT/MSHUTDOWN/RINIT/RSHUTDOWN 恰好各执行一次。</li> |
||||||
|
<li>所有可执行用户代码发生在项目请求状态和 PHP 内存池仍有效时。</li> |
||||||
|
<li>请求级指针在 RSHUTDOWN 清空;module persistent cache 在 MSHUTDOWN 清空。</li> |
||||||
|
<li>共享 hook 使用安装计数;项目私有表使用项目 namespace/static storage。</li> |
||||||
|
<li>WASM resource 必须显式释放,不依赖 Host GC 的最终时间。</li> |
||||||
|
</ul> |
||||||
|
</article> |
||||||
|
<article class="card"> |
||||||
|
<h3>禁止出现</h3> |
||||||
|
<ul> |
||||||
|
<li>在同一进程同时启动两个 native lib 所拥有的 PHP Embed runtime。</li> |
||||||
|
<li>在 <code>php_embed_shutdown()</code> 后析构持有 request zval 的 C++ 对象。</li> |
||||||
|
<li>让晚注册的 bin/lib 项目依赖 PHP 自动 RINIT/RSHUTDOWN。</li> |
||||||
|
<li>在多 ext 中使用未加项目名的全局 C++ 数据表或 runtime ABI。</li> |
||||||
|
<li>误把项目 <code>module_init()</code> 当作 Zend MINIT,或在 MINIT 中创建请求 zval。</li> |
||||||
|
</ul> |
||||||
|
</article> |
||||||
|
</div> |
||||||
|
|
||||||
|
<h3>排查顺序</h3> |
||||||
|
<ol> |
||||||
|
<li>先确认当前是 <code>bin</code>、<code>ext</code>、<code>lib</code>、WASI command 还是 component library。</li> |
||||||
|
<li>确认谁拥有 PHP runtime:外部 SAPI、生成 main、native host,还是 WIT resource。</li> |
||||||
|
<li>确认目标模块是启动前注册还是 <code>php_embed_init()</code> 后晚注册。</li> |
||||||
|
<li>为每个项目分别记录 MINIT/RINIT/RSHUTDOWN/MSHUTDOWN 次数,不要只看共享 PHPX 标志。</li> |
||||||
|
<li>崩溃若发生在 PHP memory manager shutdown,检查是否仍有项目 global、Closure、Object 或 PHPX wrapper 晚析构。</li> |
||||||
|
</ol> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section id="sources"> |
||||||
|
<h2>十、代码依据</h2> |
||||||
|
<ul class="source-list"> |
||||||
|
<li><code>compiler/src/Translator.php::genExtension()</code>:生成项目 module entry、MINIT/MSHUTDOWN/RINIT/RSHUTDOWN、<code>module_init/module_clean</code>。</li> |
||||||
|
<li><code>phpx/src/misc/typephp_main.cc</code>:原生 bin/lib 与 WASI 共用的 Embed runtime owner、晚注册模块和 shutdown 顺序。</li> |
||||||
|
<li><code>phpx/src/core/base.cc::php::request_init/request_shutdown</code>:PHPX 请求级共享状态。</li> |
||||||
|
<li><code>phpx/include/typephp_runtime.h</code>:项目名隔离的 runtime/get-module ABI 宏。</li> |
||||||
|
<li><code>compiler/src/Build/WasmInterfaceGenerator.php</code>:WIT runtime resource、单实例约束、异常边界和 resource destructor。</li> |
||||||
|
<li><code>projects/php-8.5.9/sapi/embed/php_embed.c</code>:<code>php_embed_init/shutdown</code> 内部的 SAPI/module/request 调度。</li> |
||||||
|
<li><code>projects/php-8.5.9/main/main.c::php_request_shutdown()</code>:shutdown function、析构、RSHUTDOWN、内存池销毁的标准顺序。</li> |
||||||
|
</ul> |
||||||
|
</section> |
||||||
|
|
||||||
|
<footer>此页面是内部实现文档。流程变化时,应同时更新对应代码注释、生命周期测试和本图。</footer> |
||||||
|
</main> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,401 @@ |
|||||||
|
# TypePHP 作用域管理设计 |
||||||
|
|
||||||
|
本文是 TypePHP 与 PHPX 的内部实现文档,说明当前三种作用域管理器的职责、实现方式、生命周期、性能特征和适用场景。这里的“作用域”并不是同一个 Zend 概念:callable 解析、执行帧类作用域和 `EG(fake_scope)` 分别服务于不同子系统,不能相互替代。 |
||||||
|
|
||||||
|
## 1. 设计目标 |
||||||
|
|
||||||
|
TypePHP 生成的 C++ 方法并不是普通 Zend user function。动态调用回到 ZendVM 时,Zend 仍然需要以下信息才能复现 PHP 的可见性规则: |
||||||
|
|
||||||
|
- 声明方法的词法作用域,用于判断 private/protected 成员是否可访问; |
||||||
|
- 当前 late static binding 的 called scope; |
||||||
|
- 当前实例 `$this`,用于解析非静态方法 callable; |
||||||
|
- 某些 Zend 属性、对象和异常 API 所读取的 `EG(fake_scope)`。 |
||||||
|
|
||||||
|
Scope 设计遵循以下原则: |
||||||
|
|
||||||
|
1. 优先显式传递作用域,不修改 Zend 的全局或真实执行帧状态。 |
||||||
|
2. 一个 AOT 方法调用期间只创建一次可复用的 callable context,循环中的多次调用共享它。 |
||||||
|
3. 只有编译器无法确定 callback 位置时,才临时修改最近的 user-code frame。 |
||||||
|
4. 修改 Zend executor 状态时必须使用 RAII,并保证异常路径恢复。 |
||||||
|
5. 不为纯 Native Call 或 public、绝对定位的 callback 支付额外包装成本。 |
||||||
|
|
||||||
|
## 2. 总览 |
||||||
|
|
||||||
|
| 管理器 | 管理的状态 | 主要用途 | 是否修改 Zend 当前状态 | |
||||||
|
| --- | --- | --- | --- | |
||||||
|
| `php::CallableScope` | synthetic `zend_execute_data`,包含 lexical scope、called scope 和 `$this` | 动态方法调用、first-class callable、内置函数 callback | 否 | |
||||||
|
| `php::UserCodeScopeGuard` | 最近 user-code frame 的 `zend_function::common.scope` | `call_user_func*` 及 callback 隐藏在参数展开中的动态调用路径 | 是,析构时恢复 | |
||||||
|
| `php::FakeScopeGuard` | `EG(fake_scope)` | Zend 属性、对象、异常等读取 fake scope 的 API | 是,析构或显式 `restore()` 时恢复 | |
||||||
|
|
||||||
|
选择规则可以简化为: |
||||||
|
|
||||||
|
- 能拿到明确 callable 值:使用 `CallableScope`。 |
||||||
|
- 调用 `call_user_func*`,或其他内置函数的 callback 藏在 `...$args` 中:使用 `UserCodeScopeGuard`。 |
||||||
|
- 调用的 Zend API 明确读取 `EG(fake_scope)`:使用 `FakeScopeGuard`。 |
||||||
|
- 纯 native 调用或不依赖调用者可见性的操作:不创建任何 Scope 管理器。 |
||||||
|
|
||||||
|
## 3. `php::CallableScope` |
||||||
|
|
||||||
|
### 3.1 职责 |
||||||
|
|
||||||
|
`CallableScope` 是当前普通 callable 解析的主路径。它将调用者上下文显式交给 `zend_is_callable_at_frame()`,用于: |
||||||
|
|
||||||
|
- 解析 private/protected 方法; |
||||||
|
- 解析 `self`、`parent`、`static` callback; |
||||||
|
- 保留 late static binding 的 called scope; |
||||||
|
- 为非静态方法提供真实 `$this`; |
||||||
|
- 在不修改 `EG(current_execute_data)` 和真实执行帧的前提下调用动态方法。 |
||||||
|
|
||||||
|
它不负责属性访问,也不会设置 `EG(fake_scope)`。 |
||||||
|
|
||||||
|
### 3.2 内部结构 |
||||||
|
|
||||||
|
类定义在 PHPX 的 `include/phpx.h` 中,持有: |
||||||
|
|
||||||
|
```cpp |
||||||
|
zend_function *caller_function_; |
||||||
|
zend_class_entry *called_scope_; |
||||||
|
zend_object *this_object_; |
||||||
|
mutable zend_execute_data frame_{}; |
||||||
|
``` |
||||||
|
|
||||||
|
构造时通过 `zend_vm_init_call_frame()` 初始化一个 synthetic frame: |
||||||
|
|
||||||
|
- `caller_function_->common.scope` 是 lexical scope,即声明当前方法的类; |
||||||
|
- `called_scope_` 是运行时 called scope; |
||||||
|
- 实例调用设置 `ZEND_CALL_HAS_THIS` 并携带真实 `zend_object *`; |
||||||
|
- 静态调用不携带对象,只传 called scope; |
||||||
|
- 若 called scope 为空,则回退到 lexical scope。 |
||||||
|
|
||||||
|
解析时调用: |
||||||
|
|
||||||
|
```cpp |
||||||
|
zend_is_callable_at_frame(callable, object, &frame_, 0, cache, error); |
||||||
|
``` |
||||||
|
|
||||||
|
synthetic frame 不会安装到 `EG(current_execute_data)`,因此不会污染当前 Zend 调用栈,也不需要在退出时恢复全局状态。 |
||||||
|
|
||||||
|
### 3.3 生命周期与所有权 |
||||||
|
|
||||||
|
`CallableScope` 不拥有 `zend_function`、`zend_class_entry` 或 `zend_object`,只在当前 AOT 方法栈帧内借用这些指针: |
||||||
|
|
||||||
|
- TypePHP 编译方法使用 persistent `zend_function`,其生命周期覆盖请求调用; |
||||||
|
- Closure 的 `zend_function *` 在 Closure 对象存活期间有效; |
||||||
|
- `$this` 在当前方法执行期间有效; |
||||||
|
- `CallableScope` 不可复制、不可移动,防止 synthetic frame 被意外转移或跨生命周期保存。 |
||||||
|
|
||||||
|
不得把 `CallableScope` 缓存到请求之外,也不得让它比所属方法或 Closure 活得更久。 |
||||||
|
|
||||||
|
### 3.4 编译器生成方式 |
||||||
|
|
||||||
|
编译器通过 `FunctionContext::$callableScopeVar` 延迟申请 Scope 变量。第一次需要显式 callable scope 时,`getCallableScopeExpr()` 分配临时变量;随后 `genScopeVarDecl()` 将初始化代码提升到函数入口: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::CallableScope tmp_var_1 = php::getCallableScope( |
||||||
|
get_persistent_method(...), |
||||||
|
this_ |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
`php::getCallableScope()` 根据 `this_` 同时构建 called scope 和真实实例信息。一个方法内所有 scoped call 都引用同一个 `tmp_var_1`,因此循环中的重复调用不会重复创建 synthetic frame。 |
||||||
|
|
||||||
|
如果方法从未使用 scoped dynamic call、first-class callable 或 scoped callback,编译器不会生成该变量。 |
||||||
|
|
||||||
|
### 3.5 使用入口 |
||||||
|
|
||||||
|
#### `php::callScoped()` |
||||||
|
|
||||||
|
用于动态函数或对象方法调用。内部 `call_function_impl()` 使用 `CallableScope::resolve()` 获取 `zend_fcall_info_cache`,然后执行 `zend_call_function()`。 |
||||||
|
|
||||||
|
典型场景是编译器无法将对象方法解析为 Native Call,但仍需保留当前类的 private/protected 访问权。 |
||||||
|
|
||||||
|
#### `php::makeScopedCallable()` |
||||||
|
|
||||||
|
用于 first-class callable 语法。此语法的结果必须是一个真正的 `Closure`,所以即使目标方法是 public,也不能只返回原始 callback 数组或字符串。 |
||||||
|
|
||||||
|
```php |
||||||
|
$callback = self::privateMethod(...); |
||||||
|
$callback = $this->publicMethod(...); |
||||||
|
``` |
||||||
|
|
||||||
|
普通方法通过 `zend_create_fake_closure()` 创建 Closure。若 Zend 返回 `ZEND_ACC_CALL_VIA_TRAMPOLINE`,则使用转发 Closure 保留 magic `__call()` / `__callStatic()` 的动态语义。 |
||||||
|
|
||||||
|
#### `php::prepareScopedCallback()` |
||||||
|
|
||||||
|
用于向 `array_map()`、`usort()` 等 PHP 内置函数传递 callback。这里的目标只是让内置函数正确调用 callback,不要求参数本身变成 Closure。 |
||||||
|
|
||||||
|
因此它会优先复用以下 callback 的原始值: |
||||||
|
|
||||||
|
- public 方法; |
||||||
|
- 使用绝对类名定位; |
||||||
|
- 不依赖 trampoline。 |
||||||
|
|
||||||
|
只有 private/protected 方法、`self` / `parent` / `static` 相对 callback 或 trampoline 才创建 Closure。这避免了循环中每次调用内置函数都无条件分配 fake Closure。 |
||||||
|
|
||||||
|
### 3.6 为什么仍要运行时识别 `self` / `parent` / `static` |
||||||
|
|
||||||
|
直接语法中的 `self::class` 可以在编译期展开为具体类名,但 PHP callback 也允许动态值: |
||||||
|
|
||||||
|
```php |
||||||
|
$class = 'self'; |
||||||
|
$callback = [$class, 'method']; |
||||||
|
``` |
||||||
|
|
||||||
|
此时只有运行时才能知道数组中的类名是否为相对类名。因此 `isRelativeCallableClass()` 不能完全移到编译期。对于已知的绝对 public callback,该检查会很快返回 false,并复用原值。 |
||||||
|
|
||||||
|
## 4. `php::UserCodeScopeGuard` |
||||||
|
|
||||||
|
### 4.1 职责与适用范围 |
||||||
|
|
||||||
|
`UserCodeScopeGuard` 服务于完全动态的 `call_user_func()` / `call_user_func_array()`、callback map,以及编译器无法静态改写 callback 的参数展开场景。 |
||||||
|
|
||||||
|
```php |
||||||
|
$args = [[$this, 'privateMethod'], 1]; |
||||||
|
call_user_func(...$args); |
||||||
|
``` |
||||||
|
|
||||||
|
内置函数 callback 可能位于固定位置、倒数位置、命名参数中,甚至一个函数有多个 callback。执行 `...$args` 展开前,编译器并不知道最终的 positional/named 参数布局,无法只对对应值调用 `prepareScopedCallback()`。 |
||||||
|
|
||||||
|
`call_user_func*` 本身就是 ZendVM 的完全动态调用边界,无论 callback 是否显式出现,都不创建 fake Closure。如果 callable 数组使用 `self`、`parent` 或 `static`,则先由 `normalizeCallableClass()` 将 class 部分转换为真实类名: |
||||||
|
|
||||||
|
- `self` 转为 `CallableScope::lexicalScope()`; |
||||||
|
- `parent` 转为 lexical scope 的父类; |
||||||
|
- `static` 转为 `CallableScope::calledScope()`。 |
||||||
|
|
||||||
|
规范化只复制需要修改的 callback 数组。绝对类名、对象 callback、Closure 和普通函数名保持原值。 |
||||||
|
|
||||||
|
`preg_replace_callback_array()` 是 callback map 的特例。Zend 会在函数内部逐项解析 map 中的 callback;若提前包装 map,则每次调用都要执行 O(N) 扫描,并可能触发数组 COW 和多个 Closure 分配。因此编译器保留原始 map,在方法入口创建一次 `UserCodeScopeGuard`,让 Zend 直接按正确作用域解析。 |
||||||
|
|
||||||
|
除完全动态调用、callback map 和参数展开外,普通 callback 参数不得使用此 guard;只要单个 callback 的 AST 参数位置已知,就应使用 `CallableScope` 路径。 |
||||||
|
|
||||||
|
### 4.2 实现方式 |
||||||
|
|
||||||
|
构造函数从 `EG(current_execute_data)` 开始向上查找最近的 user-code frame,并跳过 internal frame: |
||||||
|
|
||||||
|
```cpp |
||||||
|
while (frame && (!frame->func || !ZEND_USER_CODE(frame->func->type))) { |
||||||
|
frame = frame->prev_execute_data; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
找到后保存,并使用 `CallableScope::lexicalScope()` 设置可见性作用域: |
||||||
|
|
||||||
|
```cpp |
||||||
|
function_ = frame->func; |
||||||
|
previous_scope_ = function_->common.scope; |
||||||
|
function_->common.scope = callable_scope.lexicalScope(); |
||||||
|
``` |
||||||
|
|
||||||
|
析构函数恢复 `previous_scope_`。类不可复制、不可移动,保证一次构造对应一次恢复。如果没有可用的 user-code frame,会抛出: |
||||||
|
|
||||||
|
```text |
||||||
|
A user-code frame is required for scoped dynamic callback calls |
||||||
|
``` |
||||||
|
|
||||||
|
该 guard 操作的是从当前请求执行链找到的 user-code frame,不是 TypePHP 注册在 MINIT 的 persistent internal method。`EG(current_execute_data)` 本身属于当前 executor 上下文。其影响窗口被限制在当前 AOT 方法调用的 RAII 生命周期内。 |
||||||
|
|
||||||
|
### 4.3 编译器生成方式 |
||||||
|
|
||||||
|
编译器维护语义明确的标记: |
||||||
|
|
||||||
|
```php |
||||||
|
FunctionContext::$needsUserCodeCallableScope |
||||||
|
``` |
||||||
|
|
||||||
|
当编译器遇到 `call_user_func*` 的动态 callback,或一个已知会同步调用 callback 的内置函数存在无法匹配的参数展开时,`markUserCodeCallableScope()` 设置该标记。状态属于当前 `FunctionContext`,因此普通方法、嵌套 Closure 和 Fiber 各自独立,不会把 guard 错误泄漏到外层函数。每个函数体入口只生成一个: |
||||||
|
|
||||||
|
```cpp |
||||||
|
php::CallableScope tmp_var_1 = php::getCallableScope(..., this_); |
||||||
|
php::UserCodeScopeGuard tmp_var_2{tmp_var_1}; |
||||||
|
``` |
||||||
|
|
||||||
|
即使调用形态是 `call_user_func($closure)`,且 Closure 内部再次通过 |
||||||
|
`call_user_func(['self', 'method'])` 调用,每一层也只读取自己的 |
||||||
|
`FunctionContext`、lexical scope 和 `$this`,不能复用或污染外层 guard。 |
||||||
|
|
||||||
|
它不是按 call site 或循环迭代创建的。没有上述动态 callback 的方法不会产生此成本。 |
||||||
|
|
||||||
|
### 4.4 为什么当前保留该兜底 |
||||||
|
|
||||||
|
若完全移除它,编译器必须在参数展开完成后增加一套结构化参数绑定和改写流程,正确处理: |
||||||
|
|
||||||
|
- positional 与 named 参数合并; |
||||||
|
- callback 的正向和倒数位置; |
||||||
|
- 一个函数的多个 callback; |
||||||
|
- callback map; |
||||||
|
- unpack 中重复、缺失或覆盖参数时的 PHP 错误语义。 |
||||||
|
|
||||||
|
这不是一个局部替换,而是对 `parseCallArgs()` 和参数容器生成流程的中等规模重构。在完成统一的运行时参数后处理机制前,保留范围严格受控的 `UserCodeScopeGuard` 更简单可靠。 |
||||||
|
|
||||||
|
## 5. `php::FakeScopeGuard` |
||||||
|
|
||||||
|
### 5.1 职责 |
||||||
|
|
||||||
|
`FakeScopeGuard` 是 `EG(fake_scope)` 的 RAII 包装。部分 Zend API 不接受显式调用 frame,而是直接读取 `EG(fake_scope)` 来判断类成员可见性或执行类作用域相关操作。只有这些 API 才应使用它。 |
||||||
|
|
||||||
|
当前典型场景包括: |
||||||
|
|
||||||
|
- 动态属性读取、写入和属性 hook; |
||||||
|
- Zend object handler 调用; |
||||||
|
- 类作用域下的默认值或对象初始化; |
||||||
|
- 异常对象相关的 Zend 操作; |
||||||
|
- 其他明确读取 `EG(fake_scope)` 的 Zend 内部接口。 |
||||||
|
|
||||||
|
TypePHP 的属性访问生成器会通过 `FakeScopeGuard::current()` 将当前 fake scope 传给 PHPX 属性 helper。 |
||||||
|
|
||||||
|
### 5.2 实现方式 |
||||||
|
|
||||||
|
构造时保存旧值并设置新值,析构时恢复: |
||||||
|
|
||||||
|
```cpp |
||||||
|
explicit FakeScopeGuard(Scope scope) noexcept : previous_(current()) { |
||||||
|
EG(fake_scope) = scope; |
||||||
|
} |
||||||
|
|
||||||
|
~FakeScopeGuard() noexcept { |
||||||
|
restore(); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
`Scope` 通过 `decltype(EG(fake_scope))` 推导,以同时兼容 PHP 8.4 的可变指针和 PHP 8.5 的 pointer-to-const。`restore()` 是幂等操作,可以安全地提前调用一次。 |
||||||
|
|
||||||
|
### 5.3 Zend bailout 注意事项 |
||||||
|
|
||||||
|
C++ 异常展开会执行析构函数,但 Zend bailout 使用 `longjmp`,不会执行 C++ 析构函数。如果 guard 的生命周期跨越 bailout 边界,必须在对应的 `zend_catch` 路径中显式调用: |
||||||
|
|
||||||
|
```cpp |
||||||
|
fake_scope_guard.restore(); |
||||||
|
``` |
||||||
|
|
||||||
|
然后再继续 bailout 或转换异常。仅依赖析构函数处理 bailout 是错误的。 |
||||||
|
|
||||||
|
### 5.4 不适用场景 |
||||||
|
|
||||||
|
`FakeScopeGuard` 不能替代 `CallableScope`: |
||||||
|
|
||||||
|
- 它没有 synthetic frame; |
||||||
|
- 它不能携带 `$this`; |
||||||
|
- 它不能完整表达 lexical scope 与 called scope; |
||||||
|
- `zend_is_callable_at_frame()` 的解析语义不应通过全局 fake scope 间接模拟。 |
||||||
|
|
||||||
|
同样,不能为了“可能需要访问 private”而在整个 AOT 方法入口无条件设置 `EG(fake_scope)`。这会扩大全局状态的影响范围,并让无关的 native 密集调用承担成本。 |
||||||
|
|
||||||
|
## 6. 三种 Scope 的调用流程 |
||||||
|
|
||||||
|
### 6.1 已知动态方法调用 |
||||||
|
|
||||||
|
```text |
||||||
|
AOT method entry |
||||||
|
-> lazily generated CallableScope |
||||||
|
-> php::callScoped() |
||||||
|
-> CallableScope::resolve() |
||||||
|
-> zend_is_callable_at_frame(synthetic frame) |
||||||
|
-> zend_call_function() |
||||||
|
``` |
||||||
|
|
||||||
|
整个过程不修改真实 Zend frame。 |
||||||
|
|
||||||
|
### 6.2 已知内置函数 callback |
||||||
|
|
||||||
|
```text |
||||||
|
compiler marks callback argument |
||||||
|
-> prepareScopedCallback(value, CallableScope) |
||||||
|
-> public absolute callback: reuse value |
||||||
|
-> scoped/trampoline callback: create Closure |
||||||
|
-> call PHP internal function |
||||||
|
``` |
||||||
|
|
||||||
|
first-class callable 使用同一解析基础,但必须调用 `makeScopedCallable()` 并返回 Closure。 |
||||||
|
|
||||||
|
### 6.3 参数展开中的 callback |
||||||
|
|
||||||
|
```text |
||||||
|
AOT method entry |
||||||
|
-> UserCodeScopeGuard changes nearest user-code frame scope |
||||||
|
-> internal function receives expanded arguments |
||||||
|
-> Zend resolves hidden callback using that frame scope |
||||||
|
-> method exit / C++ exception unwind |
||||||
|
-> guard restores original scope |
||||||
|
``` |
||||||
|
|
||||||
|
### 6.4 属性或对象 handler |
||||||
|
|
||||||
|
```text |
||||||
|
save EG(fake_scope) |
||||||
|
-> install FakeScopeGuard |
||||||
|
-> call Zend property/object API |
||||||
|
-> restore on normal/C++ exception exit |
||||||
|
-> explicitly restore in zend_catch if bailout is possible |
||||||
|
``` |
||||||
|
|
||||||
|
## 7. 禁止混用与维护约束 |
||||||
|
|
||||||
|
1. 不要用 `FakeScopeGuard` 解析 callable。 |
||||||
|
2. 不要为普通已知 callback 修改真实 user-code frame;使用 `prepareScopedCallback()`。 |
||||||
|
3. 不要让 `UserCodeScopeGuard` 重新变成所有动态调用的通用入口。 |
||||||
|
4. 不要在循环中的 call site 重建 `CallableScope`;应由 `FunctionContext` 提升到方法入口并复用。 |
||||||
|
5. 不要缓存 `CallableScope` 借用的函数、对象或 synthetic frame 到请求之外。 |
||||||
|
6. 不要把 first-class callable 改为返回原始 callback;其 PHP 结果类型必须是 Closure。 |
||||||
|
7. 新增会同步调用 callback 的 PHP 内置函数时,需要更新 callback 参数描述表,注明位置、参数名以及是否为 callback map。 |
||||||
|
8. 保存 callback 但不立即调用的函数不能仅因接收 callable 就标记 scope fallback,例如 `spl_autoload_register()`。 |
||||||
|
9. 新增跨 Zend bailout 的 `FakeScopeGuard` 用法时,代码审查必须检查 `zend_catch` 是否显式恢复。 |
||||||
|
|
||||||
|
## 8. 性能模型 |
||||||
|
|
||||||
|
| 路径 | 主要成本 | 优化策略 | |
||||||
|
| --- | --- | --- | |
||||||
|
| `CallableScope` | 初始化一个 synthetic frame | 每个 AOT 方法最多一次,循环复用 | |
||||||
|
| `callScoped()` | `zend_is_callable_at_frame()` 动态解析 | 仅动态调用使用;可解析的 Native Call 不进入此路径 | |
||||||
|
| `prepareScopedCallback()` | 一次 callable 解析 | public 绝对 callback 不创建 Closure | |
||||||
|
| `makeScopedCallable()` | callable 解析及 Closure 分配 | 仅 first-class callable 使用 | |
||||||
|
| `UserCodeScopeGuard` | 方法入口一次指针查找、写入和退出恢复 | 只为 `call_user_func*`、callback map 或未解析的 unpack callback 生成 | |
||||||
|
| `FakeScopeGuard` | 两次 executor-global 指针赋值 | 仅包围确实读取 fake scope 的 Zend API | |
||||||
|
|
||||||
|
这套设计刻意让常见的纯 Native Call、无 callback 方法和 public callback 保持最短路径。不要为了统一表面形式而把低频 fallback 下沉到所有调用中。 |
||||||
|
|
||||||
|
## 9. 测试要求 |
||||||
|
|
||||||
|
Scope 修改至少应覆盖以下层次: |
||||||
|
|
||||||
|
- PHPX 单测:`FakeScopeGuard` 保存、嵌套、恢复和提前 `restore()`; |
||||||
|
- 编译器结构测试:一个方法只生成一个 `php::getCallableScope()`,多处调用复用同一变量; |
||||||
|
- PHPT:private/protected callback、非静态 `self::method(...)`、public callback; |
||||||
|
- PHPT:callback map 中 public 与 scoped callback 混合; |
||||||
|
- PHPT:`...$args` 中 private callback 可调用,异常退出后 scope 已恢复; |
||||||
|
- PHPT:Closure、Fiber、普通方法中的作用域生成路径; |
||||||
|
- 回归测试:纯 Native Call 不应生成额外 Scope guard。 |
||||||
|
|
||||||
|
当前相关测试包括: |
||||||
|
|
||||||
|
- `phpunit/src/ScopedCallContextTest.php` |
||||||
|
- `phpunit/code/scoped-call-context-reuse.php` |
||||||
|
- `tests/compiler/place-holder/non-static-self.phpt` |
||||||
|
- `tests/compiler/callable/scoped-internal-callbacks.phpt` |
||||||
|
- `tests/compiler/callable/unpacked-callback-scope-restored.phpt` |
||||||
|
- PHPX `tests/src/scope_guard.cpp` |
||||||
|
|
||||||
|
涉及动态调用抛出异常的 PHPT 可能触发已知 ZendVM 内存泄漏报告;只有确认泄漏来自 Zend 动态调用异常路径时,测试才可局部设置 `USE_ZEND_ALLOC=0`,不能全局关闭内存检查。 |
||||||
|
|
||||||
|
## 10. 代码位置索引 |
||||||
|
|
||||||
|
| 内容 | 位置 | |
||||||
|
| --- | --- | |
||||||
|
| `CallableScope` 及 public helper 声明 | `vendor/swoole/phpx/include/phpx.h` | |
||||||
|
| callable 解析与包装 | `vendor/swoole/phpx/src/core/base.cc`、`vendor/swoole/phpx/src/core/closure.cc` | |
||||||
|
| `FakeScopeGuard` | `vendor/swoole/phpx/include/phpx_fake_scope_guard.h` | |
||||||
|
| `UserCodeScopeGuard` | `vendor/swoole/phpx/include/typephp_helper.h`、`src/core/scope.cc` | |
||||||
|
| `php::getCallableScope()` | `vendor/swoole/phpx/include/typephp_helper.h` | |
||||||
|
| callback 标记和 Scope 变量生成 | `src/CompilerBase.php` | |
||||||
|
| callback 参数包装 | `src/Generator/CallArgumentGenerator.php` | |
||||||
|
| Closure/Fiber fallback guard | `src/Generator/ClosureGenerator.php`、`FiberGenerator.php` | |
||||||
|
| 方法 fallback guard | `src/Translator.php` | |
||||||
|
| Scope 状态 | `src/Context/FunctionContext.php` | |
||||||
|
| 属性访问中的 fake scope | `src/Parser/PropertyAccessTrait.php` | |
||||||
|
|
||||||
|
## 11. 后续演进原则 |
||||||
|
|
||||||
|
`UserCodeScopeGuard` 是复杂动态调用的长期保留机制,不以删除为目标。它修改的是当前线程、当前请求中的 user-code frame,并通过 RAII 恢复;ZTS 下不同线程拥有各自的执行上下文,因此不会共享被修改的 frame 状态。 |
||||||
|
|
||||||
|
`CallableScope` 用于编译器能够确定 callback 位置与调用边界的单一场景,以减少 frame 修改和 Closure 包装;它是一条更快、更明确的路径,而不是要求覆盖 unpack、多层动态 callback 等所有场景。遇到难以静态证明安全的组合时,应优先保留 `UserCodeScopeGuard`,不要为了形式上的统一强行改写为 `CallableScope`。 |
||||||
|
|
||||||
|
未来新增 Scope 抽象前,应先确认 Zend API 依赖的是 synthetic call frame、真实 user-code frame,还是 `EG(fake_scope)`。名称和类型应直接表达所管理的 Zend 状态,避免再次出现一个含义过宽的通用 `Scope` 类。 |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
# 测试覆盖清单 |
||||||
|
|
||||||
|
`bin/analyze-test-coverage.php` 从 PHPT 和编译器 PHPUnit fixture 的源码生成覆盖清单。它是静态测试意图分析工具,不替代测试执行。 |
||||||
|
|
||||||
|
## 使用 |
||||||
|
|
||||||
|
```bash |
||||||
|
# 终端摘要 |
||||||
|
php bin/analyze-test-coverage.php |
||||||
|
|
||||||
|
# 可审阅的完整矩阵 |
||||||
|
php bin/analyze-test-coverage.php \ |
||||||
|
--format=markdown \ |
||||||
|
--output=build/test-coverage.md |
||||||
|
|
||||||
|
# 供 CI 或其他工具读取 |
||||||
|
php bin/analyze-test-coverage.php \ |
||||||
|
--format=json \ |
||||||
|
--output=build/test-coverage.json \ |
||||||
|
--strict |
||||||
|
``` |
||||||
|
|
||||||
|
默认扫描 `tests/compiler`、`phpunit/src` 和 `phpunit/code`。也可以在命令末尾传入一个或多个 PHPT 文件或目录;`--no-phpunit` 只分析 PHPT,`--php-versions=8.4,8.5` 设置矩阵的 PHP 版本列。 |
||||||
|
|
||||||
|
`--strict` 在存在非预期的源码解析失败或无法解析的 PHPUnit fixture 引用时返回非零状态。负向数据提供器中故意不能被 php-parser 接受的样本会单独记入 `expected_parser_diagnostics`,不会伪装成工具故障。 |
||||||
|
|
||||||
|
## 三类覆盖证据 |
||||||
|
|
||||||
|
每个适用的 `PHP 版本 × 特性` 行分别记录: |
||||||
|
|
||||||
|
- `positive_compile`:有效 PHPT,或正向 PHPUnit 编译 fixture; |
||||||
|
- `runtime_semantics`:含 `EXPECT`、`EXPECTF` 或 `EXPECTREGEX` 的有效 PHPT; |
||||||
|
- `negative_diagnostic`:期待诊断的 PHPT,或明确期待失败的 PHPUnit 测试/数据提供器。 |
||||||
|
|
||||||
|
`XFAIL` 和无条件 `SKIPIF` 不计入任何证据轴。PHP 版本范围从测试标题、`SKIPIF` 中的 `PHP_VERSION_ID` 条件以及 PHPUnit 数据行中的版本字符串推断。 |
||||||
|
|
||||||
|
## 分母 |
||||||
|
|
||||||
|
报告只给出带明确分母的比率: |
||||||
|
|
||||||
|
- AST 节点覆盖分母:当前安装的 `nikic/php-parser` 所提供的具体 AST 节点种类;用于错误恢复的 `Expr_Error` 不计入。 |
||||||
|
- 特性轴覆盖分母:特性目录中 `introduced <= 目标 PHP 版本` 的行数。每个正向编译、运行语义和负向诊断轴独立计算。 |
||||||
|
|
||||||
|
工具不会把不同含义的三个轴合成一个“项目总覆盖率”。完整 JSON 同时保留特性目录、逐项证据来源、矩阵、AST 节点出现次数、解析问题和排除原因,便于 CI 进一步检查。 |
||||||
|
|
||||||
|
## 分类边界 |
||||||
|
|
||||||
|
AST 节点由 parser 自动提取。无法只靠节点区分的语义特性(例如 DNF 出现位置、属性 hook 变体、`exit(message: ...)`)由分析器中的显式特性目录补充。新增语言特性时应同时登记其引入版本和检测规则,以维持版本矩阵的明确分母。 |
||||||
@ -0,0 +1,462 @@ |
|||||||
|
# TypePHP WASM 技术方案与实施计划 |
||||||
|
|
||||||
|
> 状态:WASI 0.2 Component 与 Chrome Worker 原型已实现 |
||||||
|
> 调研日期:2026-08-07 |
||||||
|
> 当前目标:WASI 0.2(Preview 2),NTS,单线程;不支持 WASI 0.1 |
||||||
|
|
||||||
|
## 1. 文档目的 |
||||||
|
|
||||||
|
本文记录 TypePHP 支持 WebAssembly 的技术决策、功能边界、运行时架构、主要风险、验证方法和分阶段实施计划。 |
||||||
|
|
||||||
|
2026-08-07 的实现验证已经证明:精简 PHP 8.5、PHPX 核心、TypePHP 生成代码、GMP、MPFR 和 mpdecimal 可以通过 WASI SDK 静态链接为单个模块,并在 Wasmtime 中运行。可复现构建方法见 [构建 TypePHP WASI 程序](WASI_BUILD.md)。本文余下内容同时保留浏览器阶段的设计目标。 |
||||||
|
|
||||||
|
## 2. 核心结论 |
||||||
|
|
||||||
|
首个 TypePHP WASM 版本采用以下路线: |
||||||
|
|
||||||
|
```text |
||||||
|
PHP 源码 |
||||||
|
-> TypePHP 编译器 |
||||||
|
-> TypePHP 生成的 C++ |
||||||
|
-> WASI SDK 编译和静态链接 |
||||||
|
+ PHP NTS |
||||||
|
+ PHPX |
||||||
|
+ TypePHP runtime |
||||||
|
+ GMP / MPFR / mpdecimal |
||||||
|
+ PHP embed/WASI 运行时 |
||||||
|
-> typephp.wasm(WASI 0.2 command component) |
||||||
|
``` |
||||||
|
|
||||||
|
具体决策如下: |
||||||
|
|
||||||
|
1. 第一版复用当前 C++/Zend 后端,不直接生成 WAT/WASM,也不重新实现 PHP 运行时。 |
||||||
|
2. 使用 WASI SDK 的 `wasm32-wasip2` sysroot 直接生成 Component;Chrome 由 Jco 转译为 ESM,不维护第二套 Emscripten ABI。 |
||||||
|
3. PHP、PHPX、TypePHP 生成代码和高精度库全部静态链接到一个 `.wasm` 模块。 |
||||||
|
4. Wasmtime 和 Chrome 共同提供 CLI、stdio、exit、clocks、random 和受控文件系统;Chrome host 固定运行在 Worker 中。 |
||||||
|
5. 仅支持 PHP NTS,不支持线程。 |
||||||
|
6. 禁用 Fiber 和 Generator。 |
||||||
|
7. 必须支持 C++ 异常以及 Zend bailout 所需的 `setjmp/longjmp`。 |
||||||
|
8. 保留 PHP stream 框架和本地 stream,禁用网络 transport 和依赖操作系统进程能力的功能。 |
||||||
|
9. WordPress Playground 和其他 PHP-WASM 项目只作为补丁与移植经验来源,不作为 TypePHP 的依赖或代码基础。 |
||||||
|
|
||||||
|
本文描述的是最短可落地路径。长期的后端中立方案参见 [BACKEND_NEUTRAL_IR.md](BACKEND_NEUTRAL_IR.md)。WASI 原型证明无需为了 WASM 重写 TypePHP 前端和语义层。 |
||||||
|
|
||||||
|
## 3. 为什么不采用 WordPress Playground |
||||||
|
|
||||||
|
WordPress Playground 是一个成熟的浏览器 WordPress 产品,但不是小型 PHP-WASM 移植层。其仓库和构建体系同时服务于: |
||||||
|
|
||||||
|
- 多个 PHP 版本和扩展组合; |
||||||
|
- WordPress 发行版及其资源; |
||||||
|
- 浏览器、Web Worker 和 Node.js 运行时; |
||||||
|
- 虚拟文件系统、挂载和持久化; |
||||||
|
- 网络代理和浏览器 HTTP 适配; |
||||||
|
- NPM 包、网站、开发工具及集成测试; |
||||||
|
- WordPress 特有的 API 和产品功能。 |
||||||
|
|
||||||
|
TypePHP 无法直接复用 Playground 发布的 PHP-WASM 二进制,因为 TypePHP 需要把 PHPX、编译生成的 C++ 和高精度库一起静态链接。若 fork Playground,TypePHP 还会被其 monorepo、Node/NPM 构建、版本矩阵和产品发布周期绑定。 |
||||||
|
|
||||||
|
因此采用以下原则: |
||||||
|
|
||||||
|
- 不 fork WordPress Playground; |
||||||
|
- 不把 `@php-wasm/*` 作为 TypePHP 的运行时依赖; |
||||||
|
- 不复制其 WordPress、网络代理、文件同步和 UI 层; |
||||||
|
- 仅研究 PHP configure 参数、php-src 补丁、Emscripten 兼容处理和最小 C API; |
||||||
|
- 所有借用补丁必须拆分、注明来源,并验证是否仍适用于 TypePHP 固定的 PHP/Emscripten 版本。 |
||||||
|
|
||||||
|
`seanmorris/php-wasm`、`soyuka/php-wasm` 等项目也遵循相同原则:可用作构建参考和问题索引,但不成为 TypePHP 的基础仓库。 |
||||||
|
|
||||||
|
## 4. 目标与非目标 |
||||||
|
|
||||||
|
### 4.1 当前 WASI 目标 |
||||||
|
|
||||||
|
- 在 Wasmtime 等 WASI runtime 中加载 TypePHP 编译产物。 |
||||||
|
- 执行静态编译的 TypePHP 应用入口。 |
||||||
|
- 保持 TypePHP 当前基于 Zend 和 PHPX 的主要语言语义。 |
||||||
|
- 正确处理 PHP request 生命周期、C++ 异常和 Zend bailout。 |
||||||
|
- 支持 GMP、MPFR 和 mpdecimal 高精度类型。 |
||||||
|
- 支持 WASI 文件系统和必要的本地 PHP stream。 |
||||||
|
- 对不支持的功能给出确定、可测试的错误,而不是链接失败或运行时崩溃。 |
||||||
|
- 构建过程可复现,php-src、WASI SDK 和数值库版本固定。 |
||||||
|
|
||||||
|
### 4.2 首期非目标 |
||||||
|
|
||||||
|
- 无宿主适配的浏览器直接运行。 |
||||||
|
- pthread、Web Worker 并行 PHP 或共享内存。 |
||||||
|
- Fiber 和 Generator。 |
||||||
|
- 动态扩展加载。 |
||||||
|
- PHP 源码的运行时编译或通用 `eval()`。 |
||||||
|
- TCP、UDP、Unix socket 和监听端口。 |
||||||
|
- MySQL、PostgreSQL、Redis 等网络客户端。 |
||||||
|
- `curl`、FTP、SMTP 等网络协议实现。 |
||||||
|
- `fork`、`exec`、`system`、`shell_exec`、`proc_open` 和信号处理。 |
||||||
|
- FFI、JIT、opcache 和调试器。 |
||||||
|
- 完整 WordPress 兼容性。 |
||||||
|
- 在第一阶段实现异步宿主调用。 |
||||||
|
|
||||||
|
## 5. 目标平台选择 |
||||||
|
|
||||||
|
### 5.1 当前使用 WASI SDK |
||||||
|
|
||||||
|
当前先建立命令行可验证基线。WASI SDK 已经验证可以同时提供: |
||||||
|
|
||||||
|
- C/C++ 到 WebAssembly 的完整工具链; |
||||||
|
- 标准 Wasm C++ exception handling; |
||||||
|
- Zend bailout 所需的 SJLJ; |
||||||
|
- capability-based 文件系统; |
||||||
|
- libc、时间和随机数接口。 |
||||||
|
|
||||||
|
PHP、PHPX 和所有 TypePHP C++ 翻译单元必须使用一致的 Wasm EH/SJLJ 参数。链接器必须将函数签名不一致视为致命错误。 |
||||||
|
|
||||||
|
### 5.2 Chrome Component host |
||||||
|
|
||||||
|
Chrome 当前不能原生实例化 Component。构建器使用 Jco 将同一份 WASI 0.2 Component 转译为 core Wasm 与 ESM,并由 `examples/wasm-hello/typephp-worker.mjs` 演示宿主入口。浏览器适配不包含 PHP、PHPX 或高精度类型语义。 |
||||||
|
|
||||||
|
## 6. 产物和运行模型 |
||||||
|
|
||||||
|
### 6.1 发布产物 |
||||||
|
|
||||||
|
建议最小发布物为: |
||||||
|
|
||||||
|
```text |
||||||
|
dist/ |
||||||
|
├── typephp.wasm |
||||||
|
└── typephp-wasm.mjs |
||||||
|
``` |
||||||
|
|
||||||
|
所有 C/C++ 代码进入 `typephp.wasm`。浏览器自身不会自动提供 WASI imports;`typephp-wasm.mjs` 作为 WASI host/adapter 的装载入口,只负责: |
||||||
|
|
||||||
|
- 获取和实例化 `.wasm`; |
||||||
|
- 提供 stdout/stderr; |
||||||
|
- 初始化内存文件系统; |
||||||
|
- 实现或接入 WASI clocks、随机数等宿主能力; |
||||||
|
- 调用导出的 TypePHP 生命周期接口; |
||||||
|
- 把状态码和错误信息转换为 JavaScript 结果。 |
||||||
|
|
||||||
|
不应把 PHP 语义、Zend 对象操作或 TypePHP 业务逻辑放入 JavaScript loader。 |
||||||
|
|
||||||
|
### 6.2 生命周期 |
||||||
|
|
||||||
|
建议采用“模块启动一次、请求可重复执行”的模型: |
||||||
|
|
||||||
|
```text |
||||||
|
instantiate wasm |
||||||
|
-> typephp_wasm_module_startup() |
||||||
|
-> typephp_wasm_request_startup() |
||||||
|
-> TypePHP AOT entry |
||||||
|
-> typephp_wasm_request_shutdown() |
||||||
|
-> 可再次执行 request |
||||||
|
-> typephp_wasm_module_shutdown() |
||||||
|
``` |
||||||
|
|
||||||
|
每次请求必须有独立的 PHP request 内存池。执行成功、PHP 异常、C++ 异常和 Zend bailout 都必须进入统一的清理路径。 |
||||||
|
|
||||||
|
模块导出 API 可从以下最小集合开始,名称以实际实现为准: |
||||||
|
|
||||||
|
```c |
||||||
|
int typephp_wasm_module_startup(void); |
||||||
|
int typephp_wasm_run(int argc, const char **argv); |
||||||
|
const char *typephp_wasm_last_error(void); |
||||||
|
void typephp_wasm_module_shutdown(void); |
||||||
|
``` |
||||||
|
|
||||||
|
`typephp_wasm_run()` 执行已经静态链接的 AOT 入口,不负责在运行时解析和编译任意 PHP 源码。 |
||||||
|
|
||||||
|
## 7. PHP 构建策略 |
||||||
|
|
||||||
|
### 7.1 基础配置 |
||||||
|
|
||||||
|
- 固定一个明确的 php-src commit,而不是只固定分支名。 |
||||||
|
- NTS 构建。 |
||||||
|
- 禁用 CLI、CGI、FPM、Apache 等现有 SAPI。 |
||||||
|
- 新增最小 `typephp_wasm` SAPI,或先用极小的 embed 原型验证生命周期,再收敛为专用 SAPI。 |
||||||
|
- 禁用 opcache/JIT。 |
||||||
|
- 所有扩展静态链接。 |
||||||
|
- 关闭不需要的扩展和自动探测,避免宿主机环境改变构建结果。 |
||||||
|
- 用 `config.site` 和独立 patch 目录记录交叉编译结论。 |
||||||
|
|
||||||
|
首期不要直接复制其他项目的完整 configure 参数。应从最小 PHP core 启动,根据 TypePHP PHPT 和运行时依赖逐项增加扩展。 |
||||||
|
|
||||||
|
### 7.2 扩展分层 |
||||||
|
|
||||||
|
建议把扩展分为三组: |
||||||
|
|
||||||
|
1. **必须启用**:TypePHP 和 Zend 基本运行所需的 core、standard、SPL、date、pcre、hash、json 等,最终以实际链接和测试结果为准。 |
||||||
|
2. **可选本地扩展**:ctype、filter、mbstring、tokenizer、fileinfo、zlib 等,无操作系统网络依赖,但会增加体积。 |
||||||
|
3. **首期禁用**:sockets、curl、mysqli、PDO 网络驱动、pcntl、posix、FFI、shm、sysv、readline、opcache/JIT 等。 |
||||||
|
|
||||||
|
GMP、MPFR 和 mpdecimal 首先作为 PHPX/TypePHP 高精度实现的静态依赖处理,不要求启用 PHP `ext/gmp`。 |
||||||
|
|
||||||
|
## 8. PHP stream 和操作系统能力 |
||||||
|
|
||||||
|
### 8.1 不关闭整个 stream 子系统 |
||||||
|
|
||||||
|
PHP 标准库大量依赖 stream。完全关闭 stream 会破坏文件读写、`php://`、include 路径处理以及部分标准扩展,收益小而兼容成本高。 |
||||||
|
|
||||||
|
首期保留: |
||||||
|
|
||||||
|
- 普通文件 stream,底层使用 Emscripten MEMFS; |
||||||
|
- `php://memory`; |
||||||
|
- `php://temp`; |
||||||
|
- `php://stdin`、`php://stdout`、`php://stderr` 的宿主映射; |
||||||
|
- `data://` 是否启用由体积和安全评估决定; |
||||||
|
- 纯内存 stream filter 可按需启用。 |
||||||
|
|
||||||
|
### 8.2 禁用网络 stream |
||||||
|
|
||||||
|
应在 PHP 构建和运行时注册阶段禁用或不注册: |
||||||
|
|
||||||
|
- TCP、UDP 和 Unix socket transport; |
||||||
|
- socket 扩展; |
||||||
|
- `http://`、`https://`、`ftp://` 等依赖网络的 wrapper; |
||||||
|
- `fsockopen()`、`pfsockopen()`、`stream_socket_*()`; |
||||||
|
- 网络数据库和网络客户端扩展。 |
||||||
|
|
||||||
|
首期不应通过同步 XHR 或隐式 JavaScript fetch 模拟 PHP socket。若未来需要 HTTP,应设计显式、可授权的异步宿主 API,而不是伪造 POSIX socket。 |
||||||
|
|
||||||
|
### 8.3 其他 OS 相关功能 |
||||||
|
|
||||||
|
以下能力需要禁用、降级或由宿主注入: |
||||||
|
|
||||||
|
| 能力 | 首期策略 | |
||||||
|
|---|---| |
||||||
|
| 文件系统 | MEMFS;可选只读预加载文件 | |
||||||
|
| 当前目录和路径 | 虚拟根目录,禁止泄漏宿主路径 | |
||||||
|
| 环境变量 | loader 注入白名单 | |
||||||
|
| 时间 | WASI clocks;浏览器宿主使用浏览器时钟实现该接口 | |
||||||
|
| 随机数 | WASI random;浏览器宿主使用安全随机源实现,不使用弱伪随机替代 | |
||||||
|
| DNS、socket | 不支持 | |
||||||
|
| 进程、shell | 不支持 | |
||||||
|
| 信号 | 不支持 | |
||||||
|
| 用户、组、权限 | 固定值或明确报错 | |
||||||
|
| 文件锁 | 首期不支持跨实例锁;单实例内按需降级 | |
||||||
|
| 持久化 | 默认关闭;Chrome 可显式启用 OPFS 文件系统快照 | |
||||||
|
|
||||||
|
编译器应逐步增加 WASM target capability 检查:静态可识别的不支持函数在编译期报错;动态调用无法静态判断时,由运行时返回确定错误。禁止让这些调用表现为链接期缺失符号、空函数或未定义行为。 |
||||||
|
|
||||||
|
## 9. 异常、bailout 和清理 |
||||||
|
|
||||||
|
这是项目的首要技术风险,必须早于完整 PHP 功能移植进行验证。 |
||||||
|
|
||||||
|
### 9.1 编译选项 |
||||||
|
|
||||||
|
使用原生 WebAssembly exceptions 时,C 和 C++ 对 `setjmp/longjmp` 的模式必须一致。原型建议验证以下组合: |
||||||
|
|
||||||
|
```text |
||||||
|
C 编译: |
||||||
|
-sSUPPORT_LONGJMP=wasm |
||||||
|
|
||||||
|
C++ 编译: |
||||||
|
-fwasm-exceptions |
||||||
|
-sSUPPORT_LONGJMP=wasm |
||||||
|
|
||||||
|
最终链接: |
||||||
|
-fwasm-exceptions |
||||||
|
-sSUPPORT_LONGJMP=wasm |
||||||
|
``` |
||||||
|
|
||||||
|
所有 PHP、PHPX、TypePHP 和第三方 C/C++ 对象必须使用同一套 ABI 和异常配置。不能只在最终链接阶段补开 C++ exception catching。 |
||||||
|
|
||||||
|
如果目标浏览器兼容性不允许原生 Wasm EH,可研究 Emscripten JavaScript exception 模式作为备选,但不得在同一发布物中混用两套模型。 |
||||||
|
|
||||||
|
### 9.2 边界规则 |
||||||
|
|
||||||
|
- C++ 异常不得未经处理地穿过导出函数进入 JavaScript。 |
||||||
|
- Zend bailout 必须被 request 顶层捕获,并进入 request shutdown。 |
||||||
|
- bailout 后不能继续析构依赖已销毁 request 内存池的悬空 PHPX 对象。 |
||||||
|
- 栈上的 PHPX `Variant`、`Object`、`Array` 和高精度对象必须在内存池仍有效时完成析构,或由专门的 bailout 安全边界接管。 |
||||||
|
- 一个请求失败后,下一次请求必须仍可执行;否则运行时只能定义为一次性实例,并在 API 中明确。 |
||||||
|
|
||||||
|
### 9.3 必测场景 |
||||||
|
|
||||||
|
- PHP 正常返回。 |
||||||
|
- PHP `throw` 被 TypePHP 代码捕获。 |
||||||
|
- PHP 未捕获异常到达请求顶层。 |
||||||
|
- `fatalError`/Zend bailout。 |
||||||
|
- C++ `throw` 和 `catch`。 |
||||||
|
- PHP 调用 C++、C++ 再调用 PHP 时抛出异常。 |
||||||
|
- bailout 发生时栈上存在 PHPX 对象和高精度对象。 |
||||||
|
- 连续执行成功、失败、成功三个请求。 |
||||||
|
- 内存增长后再次执行请求。 |
||||||
|
|
||||||
|
## 10. 内存和高精度库 |
||||||
|
|
||||||
|
### 10.1 WASM 内存 |
||||||
|
|
||||||
|
首期使用单一线性内存,并验证 `-sALLOW_MEMORY_GROWTH`。需要记录: |
||||||
|
|
||||||
|
- 初始内存; |
||||||
|
- 最大内存; |
||||||
|
- PHP memory_limit; |
||||||
|
- request 结束后的 Zend 内存回收; |
||||||
|
- Emscripten allocator 的实际峰值; |
||||||
|
- 多次 request 后是否持续增长。 |
||||||
|
|
||||||
|
不要在没有基准测试前选择 `emmalloc`。PHP、GMP、MPFR 和 mpdecimal 都是分配密集型组件,应在 `dlmalloc`、`emmalloc` 等候选之间测试体积与运行时间。 |
||||||
|
|
||||||
|
### 10.2 GMP、MPFR 和 mpdecimal |
||||||
|
|
||||||
|
- 全部使用 Emscripten 工具链静态编译。 |
||||||
|
- 禁用汇编和宿主 CPU 专用优化。 |
||||||
|
- 固定 limb、整数宽度和 ABI 检测结果。 |
||||||
|
- 不依赖运行时动态库搜索。 |
||||||
|
- 运行现有 BigInt、BigFloat、Decimal PHPT,并增加最大内存、除零、精度、舍入和异常路径测试。 |
||||||
|
- 验证库异常或分配失败不会绕过 PHP request 清理。 |
||||||
|
|
||||||
|
## 11. 建议的仓库结构 |
||||||
|
|
||||||
|
建议在实现阶段增加独立目录,不把 Emscripten 条件散落到现有构建代码中: |
||||||
|
|
||||||
|
```text |
||||||
|
wasm/ |
||||||
|
├── README.md |
||||||
|
├── build.sh |
||||||
|
├── versions.env |
||||||
|
├── config.site |
||||||
|
├── cmake/ |
||||||
|
│ └── TypePhpWasmToolchain.cmake |
||||||
|
├── patches/ |
||||||
|
│ ├── php-src/ |
||||||
|
│ ├── gmp/ |
||||||
|
│ ├── mpfr/ |
||||||
|
│ └── mpdecimal/ |
||||||
|
├── sapi/ |
||||||
|
│ └── typephp_wasm/ |
||||||
|
├── runtime/ |
||||||
|
│ └── typephp-wasm.mjs |
||||||
|
└── tests/ |
||||||
|
``` |
||||||
|
|
||||||
|
维护原则: |
||||||
|
|
||||||
|
- patch 应小而独立,一项兼容问题一个 patch; |
||||||
|
- 每个 patch 记录上游版本、来源、原因和可删除条件; |
||||||
|
- 下载缓存不提交到 Git; |
||||||
|
- php-src、Emscripten 和第三方库使用校验和锁定; |
||||||
|
- 构建产物不进入源码仓库; |
||||||
|
- CI 至少保留 debug 和 release 两种构建。 |
||||||
|
|
||||||
|
## 12. 分阶段实施计划 |
||||||
|
|
||||||
|
### 阶段 0:工具链风险验证 |
||||||
|
|
||||||
|
目标:不接入完整 TypePHP,先证明关键底层机制可行。 |
||||||
|
|
||||||
|
- 固定 Emscripten 版本。 |
||||||
|
- 编译最小 C/C++ 混合程序。 |
||||||
|
- 验证 C++ exception。 |
||||||
|
- 验证 `setjmp/longjmp`。 |
||||||
|
- 验证两者嵌套和重复调用。 |
||||||
|
- 验证主流浏览器支持情况。 |
||||||
|
|
||||||
|
退出条件:异常和 longjmp 行为稳定,没有不可接受的浏览器缺口。 |
||||||
|
|
||||||
|
### 阶段 1:最小 PHP NTS |
||||||
|
|
||||||
|
目标:PHP core 在浏览器中完成模块和请求生命周期。 |
||||||
|
|
||||||
|
- 交叉编译最小 php-src。 |
||||||
|
- 实现最小 WASM SAPI 或 embed 验证层。 |
||||||
|
- 支持 stdout/stderr 和 MEMFS。 |
||||||
|
- 执行固定入口。 |
||||||
|
- 验证 fatal error、异常和 request shutdown。 |
||||||
|
|
||||||
|
退出条件:连续执行“成功、失败、成功”请求无崩溃、无持续内存增长。 |
||||||
|
|
||||||
|
### 阶段 2:接入 PHPX 和 TypePHP |
||||||
|
|
||||||
|
目标:现有 TypePHP C++ 后端可以由 `em++` 编译并静态链接。 |
||||||
|
|
||||||
|
- 为编译器增加 WASM platform/backend 配置。 |
||||||
|
- 统一 PHPX、TypePHP 和第三方库编译 flags。 |
||||||
|
- 链接一个最小 TypePHP `main()`。 |
||||||
|
- 建立 WASM smoke PHPT 子集。 |
||||||
|
- 为不支持的系统 API 增加 capability diagnostics。 |
||||||
|
|
||||||
|
退出条件:基础类型、函数、类、异常、数组和对象测试通过。 |
||||||
|
|
||||||
|
### 阶段 3:高精度与本地 stream |
||||||
|
|
||||||
|
目标:支持 TypePHP 关键运行时能力。 |
||||||
|
|
||||||
|
- 静态链接 GMP、MPFR、mpdecimal。 |
||||||
|
- 运行高精度完整运算符和边界测试。 |
||||||
|
- 支持必要的 `file://` 和 `php://` stream。 |
||||||
|
- 增加预加载只读资源机制。 |
||||||
|
- 明确所有被禁用的 wrapper、transport 和扩展。 |
||||||
|
|
||||||
|
退出条件:高精度测试通过,本地文件行为确定,网络 API 全部可预测地失败。 |
||||||
|
|
||||||
|
### 阶段 4:体积、性能和发布 |
||||||
|
|
||||||
|
目标:形成可分发的 TypePHP WASM SDK。 |
||||||
|
|
||||||
|
- release 优化和 dead-code elimination。 |
||||||
|
- 检查导出符号白名单。 |
||||||
|
- 比较 allocator 和内存增长配置。 |
||||||
|
- 建立下载体积、启动时间和峰值内存基准。 |
||||||
|
- 生成 `typephp.wasm` 和薄 `.mjs` loader。 |
||||||
|
- 编写用户侧功能与限制文档。 |
||||||
|
|
||||||
|
退出条件:产物可复现,兼容性清单完整,性能达到预设基线。 |
||||||
|
|
||||||
|
### 阶段 5:可选宿主能力 |
||||||
|
|
||||||
|
后续按真实需求选择,不作为基础运行时默认能力: |
||||||
|
|
||||||
|
- IDBFS 或 OPFS 持久化; |
||||||
|
- 显式 HTTP host API; |
||||||
|
- Node.js 宿主; |
||||||
|
- WASI 原型; |
||||||
|
- 多实例隔离; |
||||||
|
- Web Worker 并行实例。 |
||||||
|
|
||||||
|
每项能力都必须通过显式 capability 开启,不能让 PHP 代码默认获得宿主全部权限。 |
||||||
|
|
||||||
|
## 13. 测试策略 |
||||||
|
|
||||||
|
### 13.1 测试层次 |
||||||
|
|
||||||
|
1. **工具链测试**:exception、longjmp、静态库、链接和导出符号。 |
||||||
|
2. **PHP 生命周期测试**:module/request startup、shutdown、bailout 和重复请求。 |
||||||
|
3. **PHPX 测试**:Variant、Object、Array、引用、异常和资源析构。 |
||||||
|
4. **TypePHP PHPT**:选择不依赖 OS 的现有测试,并维护 WASM 跳过原因。 |
||||||
|
5. **高精度测试**:完整运算符、边界、错误和内存压力。 |
||||||
|
6. **能力限制测试**:网络、进程、线程和动态扩展必须稳定拒绝。 |
||||||
|
7. **浏览器测试**:Chrome、Firefox、Safari 的最低支持版本。 |
||||||
|
|
||||||
|
### 13.2 关键指标 |
||||||
|
|
||||||
|
- `.wasm` 原始大小和压缩大小; |
||||||
|
- 首次实例化时间; |
||||||
|
- module startup 和 request startup 时间; |
||||||
|
- 简单 TypePHP 程序执行时间; |
||||||
|
- 初始、峰值和多请求后的线性内存; |
||||||
|
- 异常和 bailout 后的可恢复性; |
||||||
|
- JavaScript loader 大小; |
||||||
|
- 相同输入的可复现构建校验和。 |
||||||
|
|
||||||
|
## 14. Go/No-Go 条件 |
||||||
|
|
||||||
|
出现以下任一情况,应暂停完整移植并重新评估架构: |
||||||
|
|
||||||
|
- Zend bailout 与 C++ 栈析构无法建立安全边界; |
||||||
|
- 请求失败会稳定破坏后续请求,且不能接受一次性实例模型; |
||||||
|
- GMP、MPFR 或 mpdecimal 需要大规模侵入式 fork; |
||||||
|
- `.wasm` 体积或浏览器峰值内存明显超出目标场景可接受范围; |
||||||
|
- Safari、Firefox、Chrome 需要互不兼容的异常 ABI; |
||||||
|
- PHPX 中依赖原生线程、动态链接或 OS 资源的假设无法隔离。 |
||||||
|
|
||||||
|
如果最短路径不可行,再评估 [BACKEND_NEUTRAL_IR.md](BACKEND_NEUTRAL_IR.md) 所述的独立 WASM runtime/backend,不应在没有原型数据时提前启动该重写。 |
||||||
|
|
||||||
|
## 15. 外部参考 |
||||||
|
|
||||||
|
- [PHP 源码仓库](https://github.com/php/php-src) |
||||||
|
- [Emscripten:C setjmp/longjmp 支持](https://emscripten.org/docs/porting/setjmp-longjmp.html) |
||||||
|
- [Emscripten:C/C++ 可移植性说明](https://emscripten.org/docs/porting/guidelines/portability_guidelines.html) |
||||||
|
- [Emscripten:代码与内存优化](https://emscripten.org/docs/optimizing/Optimizing-Code.html) |
||||||
|
- [WordPress Playground:编译 PHP 到 WebAssembly](https://developer.wordpress.org/playground/developers/architecture/wasm-php-compiling/) |
||||||
|
- [WordPress Playground 架构](https://wordpress.github.io/wordpress-playground/developers/architecture/) |
||||||
|
- [seanmorris/php-wasm](https://github.com/seanmorris/php-wasm) |
||||||
|
- [soyuka/php-wasm](https://github.com/soyuka/php-wasm) |
||||||
|
|
||||||
|
这些链接用于追踪上游行为和已知移植问题。TypePHP 的最终实现和兼容性必须由自己的构建、测试及基准验证,不能直接继承其他项目的结论。 |
||||||
@ -0,0 +1,228 @@ |
|||||||
|
# 构建 TypePHP WASI 程序 |
||||||
|
|
||||||
|
TypePHP 使用稳定的 WASI 0.2(Preview 2)和 Component Model。TypePHP 生成的 C++、PHPX 核心、精简的 PHP 8.5 NTS、GMP、MPFR 和 mpdecimal 会静态链接为单个 `.wasm` command 或 library component。WASI 0.1(Preview 1)不受支持。 |
||||||
|
|
||||||
|
## 环境要求 |
||||||
|
|
||||||
|
- WASI SDK 33 或更高版本(LLVM/Clang/LLD 22 或更高) |
||||||
|
- PHP 8.4 或更高版本,用于运行 TypePHP 编译器 |
||||||
|
- Wasmtime 47 或更高版本,用于运行和测试产物 |
||||||
|
- Jco 1 或更高版本,用于 browser profile;component profile 不需要 Jco |
||||||
|
- wit-bindgen-cli 0.60.0,用于 library/WasmExport 模式;command 模式不需要 |
||||||
|
- 与当前 TypePHP 版本绑定的 `wasm32-wasip2` 集成 SDK |
||||||
|
|
||||||
|
WASI SDK 的 `bin` 目录和 Wasmtime 必须加入系统 `PATH`。编译器不会探测或使用 `/opt` 等约定安装目录,也不接受专用的工具目录配置。WASI 静态库和头文件统一安装到 PHPX 的 `wasm/wasm32-wasip2/`: |
||||||
|
|
||||||
|
```bash |
||||||
|
export PATH="<wasi-sdk-bin>:<wasmtime-bin>:$PATH" |
||||||
|
``` |
||||||
|
|
||||||
|
TypePHP 使用现有的 PHPX 定位规则:优先读取 `PHPX_HOME`,其次读取 Composer 的 `swoole/phpx` 安装位置,最后使用 `vendor/swoole/phpx`。不新增 WASI 专用环境变量。 |
||||||
|
|
||||||
|
WASI 构建会检查 `wasm32-wasip2-clang`、`wasm32-wasip2-clang++`、`llvm-ar`、`llvm-ranlib`、`llvm-nm`、`wasm-component-ld` 和 `wasmtime`,并确认目标是 `wasm32-unknown-wasip2`。browser profile 另外检查 `jco`,library 模式另外检查固定版本的 `wit-bindgen`。所有工具只从 `PATH` 查找;npm script 会自动将项目本地的 `node_modules/.bin` 加入 `PATH`。 |
||||||
|
|
||||||
|
## 一条命令构建 |
||||||
|
|
||||||
|
command 模式的源文件必须提供 `main(): void`: |
||||||
|
|
||||||
|
```php |
||||||
|
<?php |
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
echo "Hello from TypePHP/WASI\n"; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
执行: |
||||||
|
|
||||||
|
```bash |
||||||
|
php bin/tpc.php --wasm hello.php |
||||||
|
``` |
||||||
|
|
||||||
|
单文件输入默认只生成当前目录下可由 Wasmtime 执行的 `hello.wasm` Component,不要求安装 Jco。生成的 `.cc` 与 host 模式使用相同的 build 目录规则,默认位于 TypePHP 根目录的 `build/`;可以使用 `--build-dir <directory>` 覆盖。 |
||||||
|
|
||||||
|
项目可以直接使用 `project.yml`: |
||||||
|
|
||||||
|
```yaml |
||||||
|
name: wasm-hello |
||||||
|
mode: bin |
||||||
|
wasm: component |
||||||
|
build-dir: build |
||||||
|
output: component/wasm-hello.wasm |
||||||
|
sources: |
||||||
|
- src |
||||||
|
``` |
||||||
|
|
||||||
|
`wasm` 只接受 `component` 或 `browser`,不接受布尔值。配置后直接执行 `php bin/tpc.php project.yml` 即可进入 WASI 构建,无需重复传入 `--wasm`。WASM 项目未配置 `target-platform` 时默认使用 `wasm32-wasip2`;`build-dir`、`output` 和 `wasm-browser-dir` 都相对于项目文件解析。完整浏览器应用见 `examples/wasm-hello/`,它显式使用 `wasm: browser`。 |
||||||
|
|
||||||
|
需要生成浏览器模块时,配置 `wasm: browser` 和 `wasm-browser-dir`,并确保 Jco 位于 `PATH`。 |
||||||
|
|
||||||
|
命令行也可以显式选择产物: |
||||||
|
|
||||||
|
- `--wasm` 或 `--wasm=component`:仅生成可由 Wasmtime 运行的 Component,不检测 Jco。 |
||||||
|
- `--wasm=browser`:生成 Component 和 Jco 浏览器模块,需要 `jco` 位于 `PATH`。 |
||||||
|
|
||||||
|
路径、sources 等详细配置继续放在 `project.yml`,不通过 `--wasm=` 传递。 |
||||||
|
|
||||||
|
PHP、PHPX、TypePHP runtime、GMP、MPFR 和 mpdecimal 由 SDK 发布阶段预编译为 WASI 静态库。应用构建只编译 TypePHP 为当前程序生成的 C++,然后链接这些 `.a`。`tpc --wasm` 不会下载源码,也不会调用 PHP、PHPX 或高精度库的构建脚本。library 模式会调用 `PATH` 中的 `wit-bindgen-cli 0.60.0` 生成当前应用的 Canonical ABI 绑定。 |
||||||
|
|
||||||
|
PHP/WASI 当前静态内建 `date`、`pcre`、`hash`、`json`、`lexbor`、`random`、`Reflection`、`SPL`、`standard`、`uri`、`ctype`、`calendar`、`bcmath`、`filter`、`tokenizer`、`mbstring`、`zlib`、`fileinfo`、`sodium`、`openssl`、`libxml`、`dom`、`SimpleXML`、`xml`、`xmlreader`、`xmlwriter`、`PDO`、`pdo_sqlite`、`zip`、`bz2` 和 `exif` 扩展。OpenSSL 采用 crypto-only 构建,不包含 TLS stream transport;HTTP/HTTPS 仍由 WASI HTTP Component 提供。 |
||||||
|
|
||||||
|
每个 C/C++ 翻译单元统一使用标准 Wasm C++ exceptions 和 WASI SJLJ;链接阶段将 ABI 警告视为错误,旧的 32 位 `zend_long` 缓存也会自动失效。 |
||||||
|
|
||||||
|
运行: |
||||||
|
|
||||||
|
```bash |
||||||
|
wasmtime hello.wasm |
||||||
|
``` |
||||||
|
|
||||||
|
Chrome Demo: |
||||||
|
|
||||||
|
```bash |
||||||
|
cd examples/wasm-hello |
||||||
|
npm ci |
||||||
|
npm run wasm |
||||||
|
npm run dev |
||||||
|
``` |
||||||
|
|
||||||
|
浏览器端始终在专用 Worker 中执行 Component。默认使用内存文件系统;发送给 Worker 的启动消息设置 `persistent: true` 后,会在启动和退出时通过 OPFS 恢复、保存文件系统快照。程序执行期间仍使用同步内存文件系统,避免每次 PHP 文件访问跨越异步 JS 边界。 |
||||||
|
|
||||||
|
## Command 与 Library 的 ZendVM 生命周期 |
||||||
|
|
||||||
|
### Command 模式 |
||||||
|
|
||||||
|
command 模式具有生成的 C++ `main()` 入口。入口依次调用: |
||||||
|
|
||||||
|
```text |
||||||
|
typephp_<project>_runtime_init(argc, argv) |
||||||
|
→ php_embed_init() |
||||||
|
→ PHP/SAPI module startup 与 MINIT |
||||||
|
→ PHP request startup 与 RINIT |
||||||
|
→ 注册并启动当前 TypePHP 应用模块 |
||||||
|
→ 当前应用的 MINIT 与 RINIT |
||||||
|
|
||||||
|
执行 TypePHP main() |
||||||
|
|
||||||
|
typephp_<project>_runtime_shutdown() |
||||||
|
→ 当前应用的 RSHUTDOWN 与模块清理 |
||||||
|
→ php_embed_shutdown() |
||||||
|
→ PHP request/module/SAPI shutdown |
||||||
|
``` |
||||||
|
|
||||||
|
调用者不需要感知这些步骤,因为生成的原生 `main()` 会自动包围整个程序生命周期。 |
||||||
|
|
||||||
|
### Library 模式必须先创建 runtime resource |
||||||
|
|
||||||
|
library component 没有可自动执行的 `main()`,单纯实例化 `.wasm` 只完成 Component 和 C/C++ Runtime 的实例化,不代表 ZendVM request 已经可用。Host 必须先调用生成的 WIT 函数: |
||||||
|
|
||||||
|
```wit |
||||||
|
create-runtime: func() -> result<runtime, typephp-error>; |
||||||
|
``` |
||||||
|
|
||||||
|
浏览器中对应的调用为: |
||||||
|
|
||||||
|
```js |
||||||
|
const component = await instantiate(null, wasi.getImportObject()); |
||||||
|
const runtime = await component.api.createRuntime(); |
||||||
|
|
||||||
|
try { |
||||||
|
const result = await runtime.someExportedFunction(); |
||||||
|
} finally { |
||||||
|
runtime[Symbol.dispose](); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
`createRuntime()` 内部通过 `TYPEPHP_RUNTIME_INIT(<project>)(1, argv)` 调用项目级初始化符号。Host |
||||||
|
只需要调用这一层稳定接口,不应直接调用 `php_embed_init()`、MINIT、RINIT 或任何 Zend C API。 |
||||||
|
|
||||||
|
当前初始化顺序如下: |
||||||
|
|
||||||
|
1. `php_embed_init()` 初始化 Embed SAPI、PHP 核心和静态扩展,并启动 PHP request;PHP 核心与已经注册的静态扩展在这里完成 MINIT/RINIT。 |
||||||
|
2. 设置 PHPX 的异常桥接,使 PHP 异常可以安全返回到生成的 WIT `result`。 |
||||||
|
3. 取得当前 TypePHP 应用的 `zend_module_entry`,调用 `zend_register_module_ex()` 和 `zend_startup_module_ex()`,完成应用模块注册与 MINIT。 |
||||||
|
4. 注册标准流并设置请求路径等 SAPI 请求信息。 |
||||||
|
5. 因为 Embed request 和请求内存池此时已经启动,生成代码会显式调用当前应用模块的 `request_startup_func`,补做该模块的 RINIT;RINIT 再初始化 TypePHP 请求级全局变量和类静态数据,完成后才返回 `runtime` resource。 |
||||||
|
|
||||||
|
这里“手动”调用的是 Host 可见的 `create-runtime()`,而不是让用户手动拼装 ZendVM 生命周期。MINIT/RINIT 的具体调用及其先后顺序全部封装在 PHPX 和生成的 Component adapter 中。 |
||||||
|
|
||||||
|
### 导出调用共享同一个 request |
||||||
|
|
||||||
|
同一 `runtime` resource 上的所有 `#[WasmExport]` 调用共享一次 RINIT 建立的 Zend request: |
||||||
|
|
||||||
|
- 不会在每次函数调用前后重复执行 RINIT/RSHUTDOWN。 |
||||||
|
- PHP request 内存池、请求级全局变量和静态状态会持续到 resource 被释放。 |
||||||
|
- 当前仅支持 NTS;同一个 runtime 上的调用必须串行,生成的 adapter 会拒绝并发或重入调用。 |
||||||
|
- 普通 PHP 异常会被转换为 WIT `result` 错误,runtime 仍然可以继续使用。 |
||||||
|
- Zend bailout 表示请求状态可能已经损坏,adapter 会将 runtime 标记为 failed,后续调用会被拒绝,直到 resource 被释放。 |
||||||
|
|
||||||
|
### 释放 resource 才会执行 RSHUTDOWN |
||||||
|
|
||||||
|
释放 WIT `runtime` resource 会通过 `TYPEPHP_RUNTIME_SHUTDOWN(<project>)()` 调用项目级关闭符号: |
||||||
|
|
||||||
|
1. 调用当前 TypePHP 应用模块的 RSHUTDOWN,清理 TypePHP 请求级对象和全局数据。 |
||||||
|
2. 注销并关闭当前应用模块,执行相应模块清理。 |
||||||
|
3. 调用 `php_embed_shutdown()`,完成其余扩展的 request shutdown、module shutdown 和 SAPI shutdown。 |
||||||
|
4. 最后释放 request 内存池,避免 PHP/CPP 包装对象在内存池消失后继续析构。 |
||||||
|
|
||||||
|
不要只依赖 JavaScript GC 触发 resource finalizer。浏览器和 Node Host 应在 `finally` 中显式调用 `runtime[Symbol.dispose]()`;Wasmtime 或其他 Host binding 也应显式 drop resource。直接终止 Worker 或进程会回收整个 Wasm 实例,但不保证 PHP 的 RSHUTDOWN/MSHUTDOWN 回调得到执行,因此不能把必须持久化的数据只放在关闭回调中。 |
||||||
|
|
||||||
|
一个 Component 实例当前只允许同时存在一个活动的 runtime resource。释放完成后可以重新创建;初始化失败或发生 Zend bailout 时,应先释放旧 resource,而不是继续调用导出函数。 |
||||||
|
|
||||||
|
## 高精度类型 |
||||||
|
|
||||||
|
WASI 产物包含 TypePHP 的三种语言级高精度类型: |
||||||
|
|
||||||
|
- `BigInt`:GMP 6.3.0 |
||||||
|
- `BigFloat`:MPFR 4.2.2 |
||||||
|
- `Decimal`:mpdecimal 4.0.1 |
||||||
|
|
||||||
|
完整示例位于 [high-precision.php](../examples/high-precision.php)。构建并运行: |
||||||
|
|
||||||
|
```bash |
||||||
|
php bin/tpc.php --wasm examples/high-precision.php |
||||||
|
wasmtime -S http high-precision.wasm |
||||||
|
``` |
||||||
|
|
||||||
|
预期输出: |
||||||
|
|
||||||
|
```text |
||||||
|
1111111101111111110111111111010 |
||||||
|
1000000000000000000000000000001 |
||||||
|
12348.14159265358979324 |
||||||
|
``` |
||||||
|
|
||||||
|
wasm32 使用 32 位指针,但 PHP 的 `zend_long` 保持 64 位,以维持 TypePHP 与 64 位 PHP 的整数语义。GMP 和 mpdecimal 使用 32 位 limb;这不改变任意精度语义,但大数吞吐量低于具有汇编优化的原生 64 位构建。 |
||||||
|
|
||||||
|
## 当前平台边界 |
||||||
|
|
||||||
|
- 仅支持 NTS、单线程。 |
||||||
|
- Fiber 和 Generator 被禁用;编译器在发现 `yield` 时直接报致命错误。 |
||||||
|
- PHPX Facade API 在 `__wasi__` 下整体禁用。PHPX 核心类型和 `phpx_std` 仍可使用。 |
||||||
|
- 不支持动态扩展、网络 socket、进程、shell 和信号。静态可识别的调用会在编译期报致命错误。 |
||||||
|
- 保留 PHP stream 框架、本地文件能力以及由 WASI host 提供的时间和随机数能力。 |
||||||
|
- command component 可由 Wasmtime 直接运行;library component 需要 Host 按 WIT 接口调用 `create-runtime()` 和导出函数。Chrome 使用 Jco 生成的 ESM 和 `examples/wasm-hello/typephp-worker.mjs` 中的 Worker host。 |
||||||
|
|
||||||
|
PHPX Facade 只是为 PHP 可选扩展生成的便捷包装,并非 TypePHP ABI 的组成部分。WASI 下整体关闭它,可以避免把 curl、socket、Swoole 等不可用 API 暴露为“可编译但链接失败”的接口;PHP/WASI 静态内建扩展本身不受 Facade 开关影响。 |
||||||
|
|
||||||
|
## WASI SDK 目录 |
||||||
|
|
||||||
|
集成 SDK 使用唯一、完整的前缀,位于 PHPX 根目录的 `wasm/wasm32-wasip2/`: |
||||||
|
|
||||||
|
```text |
||||||
|
phpx/wasm/wasm32-wasip2/ |
||||||
|
├── include/php/ # PHP 安装头文件 |
||||||
|
├── include/phpx/ # PHPX 和 TypePHP runtime 头文件 |
||||||
|
├── include/gmp.h ... |
||||||
|
├── lib/libphp.a |
||||||
|
├── lib/libphpx.a |
||||||
|
├── lib/libgmp.a |
||||||
|
├── lib/libgmpxx.a |
||||||
|
├── lib/libmpfr.a |
||||||
|
├── lib/libmpdec.a |
||||||
|
├── lib/libmpdec++.a |
||||||
|
└── .typephp-wasi-sdk-abi |
||||||
|
``` |
||||||
|
|
||||||
|
普通用户通过 TypePHP/PHPX 集成安装包获得该目录。TypePHP 开发者需要自行 clone 与当前版本绑定的 `php-8.5.9-wasm` 和 PHPX 源码,并通过 `wasm/build-sdk.sh` 组装完整 SDK。PHP/WASI 只负责 PHP;PHPX 负责 GMP、MPFR、其专属的 mpdecimal 以及 PHPX runtime。所有产物安装到同一个 PHPX checkout。若 PHPX 不在 `vendor/swoole/phpx`,继续使用已有的 `PHPX_HOME` 指向该 checkout。 |
||||||
|
|
||||||
|
不提供单独覆盖 `libphp.a`、`libphpx.a` 或数值库的路径;所有库、头文件和 `.typephp-wasi-sdk-abi` 必须来自同一次兼容构建,避免混用不同的 `zend_long`、C++ exceptions、SJLJ 或 Component Model ABI。 |
||||||
@ -0,0 +1,986 @@ |
|||||||
|
# TypePHP 与 Python 语言级互调用设计 |
||||||
|
|
||||||
|
> 状态:核心设计已确认,按 `python/implementation-plan.md` 分阶段实施。 |
||||||
|
> |
||||||
|
> 本文是语法、类型语义、运行时边界和兼容性目标的设计规范;尚未确认的细节继续在文末维护。 |
||||||
|
|
||||||
|
## 1. 目标 |
||||||
|
|
||||||
|
TypePHP 应在语言层面提供从 TypePHP 调用 Python 包的能力: |
||||||
|
|
||||||
|
1. TypePHP 导入 Python 模块,访问模块成员,调用 Python 函数和类。 |
||||||
|
2. TypePHP 操作 Python 对象,包括属性、方法、下标、迭代、运算符和调用协议。 |
||||||
|
3. TypePHP 函数、闭包和对象可以作为 Python 调用的参数,并允许 Python 在该次动态调用关系中同步回调。 |
||||||
|
4. 两个 VM 在同一进程内直接互调用,不通过 JSON、RPC 或子进程。 |
||||||
|
5. 默认保留 Python 对象身份和类型信息,避免不必要的深拷贝。 |
||||||
|
6. 语法面向普通 TypePHP/PHP 开发者,常规调用不要求理解 CPython C API、GIL 或引用计数。 |
||||||
|
7. 本功能是可选的扩展级能力;不使用 Python 语法的项目不依赖 phpy。 |
||||||
|
|
||||||
|
其中最主要的语言变化是 Python 特殊根命名空间。全局命名空间中的 `python\module\member()`,或其他命名空间中的 `\python\module\member()`,可以直接访问模块成员;`use python\module` 完全按照 PHP 的普通 namespace alias 规则工作,编译器不对 `use` 语句进行 Python 特殊处理。两种形式都把 phpy 原本需要手写的 `PyCore::import('module')` 和返回变量提升为编译期可识别的 lazy module binding。Python 对象的属性、方法、下标、迭代、参数转换、返回包装和异常等能力原则上复用 phpy 已有实现,不在 TypePHP 中重新建立一套运行时。 |
||||||
|
|
||||||
|
非目标: |
||||||
|
|
||||||
|
- 不编译 Python 源码,也不试图替代 CPython。 |
||||||
|
- 不承诺将动态 Python API 静态类型化。 |
||||||
|
- 永久不支持 Python 线程、`asyncio` 或 CPython subinterpreter。 |
||||||
|
- 不生成 Python extension,不向 Python 注册 TypePHP 函数、类或模块。 |
||||||
|
- 不提供 `#[PythonExport]` 或其他 TypePHP 符号导出机制。 |
||||||
|
- 不追求兼容 Python 语法;目标是让 TypePHP 程序方便、可靠地调用 Python 包。 |
||||||
|
- 不将任意 Python 容器自动、递归地复制为 TypePHP 数组。 |
||||||
|
|
||||||
|
## 2. 参考设计 |
||||||
|
|
||||||
|
### 2.1 Mojo |
||||||
|
|
||||||
|
Mojo 使用未经修改的 CPython 运行时保证 Python 生态兼容性,并用统一的 `PythonObject` 包装动态 Python 值。TypePHP 只借鉴其嵌入和对象包装设计,不采用其导出机制。 |
||||||
|
|
||||||
|
可借鉴的部分: |
||||||
|
|
||||||
|
- Python 值默认保持为包装对象。 |
||||||
|
- TypePHP 基础值传入 Python 时可自动转换。 |
||||||
|
- Python 值转回 TypePHP 原生类型时显式转换。 |
||||||
|
- 动态 Python 值使用统一代理类型承载。 |
||||||
|
|
||||||
|
参考:[Mojo Python interoperability](https://docs.modular.com/stable/mojo/manual/python/)、[Mojo Python types](https://docs.modular.com/mojo/manual/python/types)。 |
||||||
|
|
||||||
|
### 2.2 pybind11 |
||||||
|
|
||||||
|
pybind11 明确区分对象所有权、返回值策略、解释器生命周期、GIL guard、位置参数和关键字参数。其经验说明:跨语言调用最危险的部分不是调用语法,而是对象生命周期和异常路径。 |
||||||
|
|
||||||
|
TypePHP 不应把 pybind11 的所有权策略暴露给普通用户,但运行时必须建立同等严格的内部契约。 |
||||||
|
|
||||||
|
参考:[pybind11 embedding](https://pybind11.readthedocs.io/en/stable/advanced/embedding.html)、[pybind11 functions](https://pybind11.readthedocs.io/en/stable/advanced/functions.html)。 |
||||||
|
|
||||||
|
### 2.3 PyO3 |
||||||
|
|
||||||
|
PyO3 使用 GIL token 和带生命周期的 Python 对象指针,从类型系统上区分持有对象、借用对象和 GIL 绑定对象。 |
||||||
|
|
||||||
|
TypePHP 无需向用户暴露生命周期参数,但 phpy 的 C++ 层应借鉴这一点:所有 CPython API 调用必须能证明当前持有 GIL,所有 `PyObject*` 必须明确是 owned、borrowed 还是 stolen reference。 |
||||||
|
|
||||||
|
参考:[PyO3 object model](https://pyo3.rs/main/doc/pyo3/)、[PyO3 Python object types](https://pyo3.rs/main/types)。 |
||||||
|
|
||||||
|
## 3. phpy 的定位 |
||||||
|
|
||||||
|
phpy 是本功能的运行时基础候选,而不是已经验证完成的稳定依赖。 |
||||||
|
|
||||||
|
可复用能力包括: |
||||||
|
|
||||||
|
- 在 ZendVM 进程内初始化 CPython。 |
||||||
|
- `zval` 与 `PyObject*` 的边界转换。 |
||||||
|
- Python 模块、对象、字符串、序列、字典、集合、迭代器和 callable 的代理对象。 |
||||||
|
- TypePHP/PHP 闭包传入 Python后的 callable 代理。 |
||||||
|
- Python 异常到 Zend 异常的基础映射。 |
||||||
|
- Python 同步调用由 ZendVM 主动传入的函数、对象和 callable 代理的基础设施。 |
||||||
|
- GIL RAII guard 的雏形。 |
||||||
|
|
||||||
|
但是不能直接假定现有实现完全正确。后续实施必须同时 review phpy、重构边界、增加测试、修复 BUG 和优化性能。 |
||||||
|
|
||||||
|
设计阶段已经识别出的重点审计项: |
||||||
|
|
||||||
|
- CPython 初始化、重复初始化、关闭顺序和仍存活对象的析构。 |
||||||
|
- 每个 CPython API 的 owned/borrowed/stolen reference 规则。 |
||||||
|
- 所有成功路径和异常路径的 `Py_INCREF/Py_DECREF` 对称性。 |
||||||
|
- GIL 获取、重入调用和 TypePHP 回调 Python 再回调 TypePHP 的行为。 |
||||||
|
- 转换过程已改为每次顶层转换创建独立的 C++ 转换器对象;转换策略、递归栈和深度限制均为对象内状态,并由 RAII 恢复,不再使用进程级或线程级临时函数指针。仍需继续审计跨 VM 回调和生命周期边界。 |
||||||
|
- Python 异常转 Zend 异常后,CPython error indicator 是否始终被正确清理。 |
||||||
|
- Zend 异常转 Python 异常时,原始异常类型、消息和 traceback 的保存。 |
||||||
|
- 运算符协议是否正确。例如 PHP `/` 不能映射为 Python floor division。 |
||||||
|
- Python 大整数、无效 UTF-8、包含 NUL 的 bytes、递归容器和循环引用。 |
||||||
|
- Python 代理持有 Zend 对象时,Zend GC 与 CPython GC 之间可能形成的跨 VM 引用环。 |
||||||
|
- Python 线程、`asyncio`、subinterpreter 必须被永久、显式拒绝,而不是产生未定义行为。 |
||||||
|
|
||||||
|
TypePHP 通过 ZendVM 动态调用 phpy 扩展公开的 `PyCore`、`PyObject`、`PyDict` 等 Facade,不直接链接 `libphpy.so`,也不生成任何 phpy C++ 符号引用。现有公开名称必须保留,TypePHP 不建立第二套用户可见命名体系。 |
||||||
|
|
||||||
|
职责边界: |
||||||
|
|
||||||
|
- phpy 负责所有运行时问题:CPython 初始化、GIL、引用计数、对象代理、类型转换、异常和双 VM 生命周期。 |
||||||
|
- phpy 负责提供稳定、可测试的 Zend internal class/function/object-handler API。 |
||||||
|
- TypePHP 只负责识别语言语法、静态类型和求值顺序,并生成基于 `zend_function*` 与 PHPX/Zend 通用对象 API 的动态调用。 |
||||||
|
- TypePHP 不直接操作裸 `PyObject*`,不复制 phpy 的 GIL、引用计数或异常实现。 |
||||||
|
- 修复运行时 BUG 时优先修复 phpy,不能只在 TypePHP 生成代码中增加补丁绕过。 |
||||||
|
|
||||||
|
最小适配原则: |
||||||
|
|
||||||
|
- TypePHP 的核心新增能力是 Python `use` 解析、模块别名符号和对应代码生成。 |
||||||
|
- `python\name()`、`module\name`、`module\name()` 和运算符 lowering 都应落到 phpy 的 Zend Facade;Python 运算符通过标准库 `operator` module 调用完整的 CPython 运算协议。 |
||||||
|
- phpy 已正确解决的行为只补测试并复用;只有 review 或测试证明存在 BUG、隐式转换不符合 TypePHP 规则,或者缺少 Zend 动态入口时,才修改 phpy。 |
||||||
|
- TypePHP 不实现 CPython 协议细节,不在生成代码中复制 `PyCore`、`PyObject` 或 `PyModule` 的逻辑。 |
||||||
|
|
||||||
|
## 4. 可选扩展与运行时检测 |
||||||
|
|
||||||
|
Python 互调用是扩展级特性,不是 TypePHP 核心程序的强制依赖。 |
||||||
|
|
||||||
|
- TypePHP 生成代码只依赖 ZendVM/PHPX,不 include phpy 头文件,也不链接 `libphpy.so`。 |
||||||
|
- 编译器识别 Python 语法并保留逻辑上的 `PyObject` 类型信息,但不检查 phpy SDK、动态库、ABI 或 Python module 是否存在。 |
||||||
|
- phpy 必须像普通 PHP 扩展一样由运行环境加载并注册 `PyCore`、`PyObject` 等 Zend internal classes。 |
||||||
|
- 首次实际使用 Python 符号时,TypePHP 通过 class map/func map 解析 `PyCore` 和对应的 `zend_function*`。 |
||||||
|
- phpy 未加载时,Zend class lookup 抛出可捕获的 PHP `Error`;若未捕获,则按普通 PHP 规则成为 fatal error。 |
||||||
|
- phpy 已加载但 Python module 不存在时,`PyCore::import()` 通过 phpy 抛出 `PyError`。 |
||||||
|
- 只有 `use python\sys` 而没有实际访问任何 Python 符号时,不发生运行时解析,因此即使没有安装 phpy 也不会报错。 |
||||||
|
|
||||||
|
这种模型使同一个 TypePHP 二进制可以在未安装 phpy 的环境中运行不涉及 Python 的路径,也避免 TypePHP 与 phpy 建立原生 C++ ABI 依赖。 |
||||||
|
|
||||||
|
### 4.1 TypePHP 代码隔离 |
||||||
|
|
||||||
|
TypePHP 中所有 Python 专用实现必须集中到独立子目录,暂定为: |
||||||
|
|
||||||
|
```text |
||||||
|
src/Python/ |
||||||
|
``` |
||||||
|
|
||||||
|
该目录负责: |
||||||
|
|
||||||
|
- `python` 特殊根命名空间识别。 |
||||||
|
- import/module symbol 表。 |
||||||
|
- Python Zend class/method 名称和逻辑返回类型映射。 |
||||||
|
- Python 语法糖和静态返回类型映射。 |
||||||
|
- Python 调用、属性、下标、迭代和运算符的 C++ lowering。 |
||||||
|
- Python 专用诊断。 |
||||||
|
|
||||||
|
通用 Parser、TypeSystem、Optimizer 和 Generator 只允许保留最小、稳定的扩展入口,不应散落 `if ($isPython...)` 特判。Python 功能未启用时,不加载 Python 专用分析器,也不改变现有代码生成路径。 |
||||||
|
|
||||||
|
测试同样独立组织,建议使用: |
||||||
|
|
||||||
|
```text |
||||||
|
phpunit/src/Python/ |
||||||
|
phpunit/code/python/ |
||||||
|
tests/compiler/python/ |
||||||
|
``` |
||||||
|
|
||||||
|
具体目录名在 coding 计划阶段确认,但“实现与测试隔离”是设计约束。 |
||||||
|
|
||||||
|
## 5. 总体运行时模型 |
||||||
|
|
||||||
|
采用以下模型: |
||||||
|
|
||||||
|
- 一个进程内同时存在一个 ZendVM 和一个 CPython 主解释器。 |
||||||
|
- CPython 完全通过 phpy 已有的扩展生命周期初始化和关闭;TypePHP 不建立第二套初始化路径。 |
||||||
|
- 所有 Python API 边界自动获取 GIL,普通用户不操作 GIL。 |
||||||
|
- `PyObject` 及其 `PyDict`、`PyList`、`PyStr` 等子类持有 CPython strong reference。 |
||||||
|
- Python 代理对象复制时增加引用计数,析构时在合法的解释器/GIL 上下文中减少引用计数。 |
||||||
|
- borrowed reference 只允许存在于 phpy 内部的短生命周期作用域,不暴露给 TypePHP。 |
||||||
|
- TypePHP 调用 Python、Python 同步回调由 TypePHP 作为参数传入的 callable、该 callable 再调用 Python,必须支持同步重入。 |
||||||
|
- Python 不能独立导入 TypePHP 应用,也不能通过全局注册表查找 TypePHP 函数或类型。 |
||||||
|
|
||||||
|
解释器关闭前必须先释放所有由 TypePHP 持有的 Python 对象。不能依赖 `Py_Finalize()` 自动修复错误的生命周期。 |
||||||
|
|
||||||
|
## 6. 模块名称与导入语法 |
||||||
|
|
||||||
|
`python` 是编译器识别的保留根命名空间: |
||||||
|
|
||||||
|
```php |
||||||
|
python\math\sqrt(16); |
||||||
|
python\os\path\join('/tmp', 'file.txt'); |
||||||
|
|
||||||
|
use python\sys; |
||||||
|
use Python\numpy as np; |
||||||
|
use python\numpy\linalg as linalg; |
||||||
|
``` |
||||||
|
|
||||||
|
完整名称不要求先写 `use`: |
||||||
|
|
||||||
|
```php |
||||||
|
$root = python\math\sqrt(16); |
||||||
|
$pi = Python\math\pi; |
||||||
|
``` |
||||||
|
|
||||||
|
最后一个 `\` 之前、Python 根之后的所有片段均构成 Python module path,最后一个片段是模块成员。PHP 的 `\` 在导入时转换为 Python 的 `.`。因此全局命名空间中的 `python\os\path\join()` 明确表示 module `os.path` 的 `join` callable。 |
||||||
|
|
||||||
|
Python module 名称仍严格服从 PHP 的 namespace 解析规则。位于普通 PHP namespace 内时,完整模块名必须使用前导 `\`: |
||||||
|
|
||||||
|
```php |
||||||
|
namespace App; |
||||||
|
|
||||||
|
\python\math\sqrt(16); // Python module math |
||||||
|
python\math\sqrt(16); // 普通 PHP 名称 App\python\math\sqrt,不是 Python module |
||||||
|
``` |
||||||
|
|
||||||
|
这是 PHP 语法的一部分,`python` 不作为例外绕过当前 namespace。`use python\math;` 与其他 PHP `use` 声明一样从根名称导入,因此在 namespace 内也可以使用 alias 简写。 |
||||||
|
|
||||||
|
`use` 仅用于缩短完整名称,不是访问 Python module 的前置条件: |
||||||
|
|
||||||
|
PHP 的 `use function` 和 `use const` 同样适用,并支持普通的 `as` alias: |
||||||
|
|
||||||
|
```php |
||||||
|
use function python\len; |
||||||
|
use function python\math\sqrt as py_sqrt; |
||||||
|
use const python\math\pi as py_pi; |
||||||
|
|
||||||
|
$length = len([1, 2, 3]); |
||||||
|
$root = py_sqrt(16); |
||||||
|
$pi = py_pi; |
||||||
|
``` |
||||||
|
|
||||||
|
这些声明仍完全由 PHP 名称解析处理。TypePHP 只在 `FuncCall` 或 `ConstFetch` 的最终完整名称位于根命名空间 `python\...` 时进入 Python lowering;`use` 声明本身不会导入 Python module。 |
||||||
|
|
||||||
|
分别等价于: |
||||||
|
|
||||||
|
```python |
||||||
|
import sys |
||||||
|
import numpy as np |
||||||
|
import numpy.linalg as linalg |
||||||
|
``` |
||||||
|
|
||||||
|
在现有 phpy PHP API 中,语义上对应: |
||||||
|
|
||||||
|
```php |
||||||
|
$sys = PyCore::import('sys'); |
||||||
|
$np = PyCore::import('numpy'); |
||||||
|
$linalg = PyCore::import('numpy.linalg'); |
||||||
|
``` |
||||||
|
|
||||||
|
`PyCore::import()` 返回一个 `PyModule`/`PyObject` 变量,后续属性和方法均通过该变量访问。`use python\module` 只是普通 PHP namespace alias,不立即执行导入,也不生成 ZendVM class、namespace 或用户可见变量。编译器仅在处理函数调用或常量读取时检查 PHP 已解析的完整名称。 |
||||||
|
|
||||||
|
当编译器在函数代码中发现 `module\attr` 或 `module\func()` 时,采用与现有 `funcMap` 相同的编译器结构:为实际使用的完整 module 名称分配整数 ID,生成统一的 `THREAD_LOCAL` zval array,并通过 lazy getter 动态调用 `PyCore::import()`。下列名称只是设计示意: |
||||||
|
|
||||||
|
```cpp |
||||||
|
THREAD_LOCAL zval php_python_module_map[module_count]; |
||||||
|
|
||||||
|
php::Object php_get_python_module(int module_id, const php::Str &module_name) |
||||||
|
{ |
||||||
|
zval *module = &php_python_module_map[module_id]; |
||||||
|
if (UNEXPECTED(Z_ISUNDEF_P(module))) { |
||||||
|
// Resolve PyCore::import through classMap/funcMap and invoke zend_function*. |
||||||
|
php::Variant value = php::call(/* cached zend_function* */, php::ArgList{module_name}); |
||||||
|
ZVAL_COPY(module, value.ptr()); |
||||||
|
} |
||||||
|
return php::Object(module); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
对应 lowering: |
||||||
|
|
||||||
|
```text |
||||||
|
use Python\numpy as np |
||||||
|
-> compile-time namespace marker: np => "numpy" |
||||||
|
-> module id allocated only when np is actually referenced |
||||||
|
|
||||||
|
np\version |
||||||
|
-> php::Object(php_get_python_module(module_id, "numpy")).attr("version") |
||||||
|
|
||||||
|
np\array($value) |
||||||
|
-> php::Object(php_get_python_module(module_id, "numpy")).call("array", converted($value)) |
||||||
|
|
||||||
|
python\numpy\array($value) |
||||||
|
-> the same module id and lowering as np\array($value) |
||||||
|
|
||||||
|
python\os\path\join($left, $right) |
||||||
|
-> php::Object(php_get_python_module(module_id, "os.path")).call("join", ...) |
||||||
|
``` |
||||||
|
|
||||||
|
同一完整 module 名称在整个 TypePHP 构建中只分配一个 ID;完整名称和任意 `use` 别名引用同一 module 时也共享该 ID。如果当前 `.php` 文件只有 `use python\sys`,但没有出现任何 `sys\attr`、`sys\func()` 或其他 `sys` 符号访问,则编译器不为它分配 module ID,运行时不调用 `import('sys')`,也不会因为 Python 环境缺少该 module 而报错。 |
||||||
|
|
||||||
|
未使用 module 不触发任何 phpy 运行时解析。`tpc` 只检查 `use python\sys` 本身的语法和别名冲突,不检查 phpy SDK/ABI,也不增加 phpy 链接依赖。 |
||||||
|
|
||||||
|
### 6.1 与 `funcMap` 的关系 |
||||||
|
|
||||||
|
`pythonModuleMap` 复用 `funcMap` 已验证的整体模式: |
||||||
|
|
||||||
|
- 编译期使用 `完整 module 名称 → integer ID` 的 map 去重。 |
||||||
|
- 数据声明集中生成,普通 `.cc` 只引用 extern array 和 getter。 |
||||||
|
- getter 首次访问时初始化,后续通过数组直接命中。 |
||||||
|
- 只为真正出现成员访问或调用的 module 分配 ID。 |
||||||
|
- 在应用/request clean 阶段集中清理。 |
||||||
|
|
||||||
|
但是两者不能机械地使用完全相同的清理代码: |
||||||
|
|
||||||
|
- `funcMap` 保存由 Zend function table 拥有的 non-owning `zend_function*`,清理时可以直接 `memset`。 |
||||||
|
- `pythonModuleMap` 保存 phpy 返回的 Zend `PyModule` object zval,不能直接 `memset` 覆盖有效对象。 |
||||||
|
- request clean 必须逐项执行 `zval_ptr_dtor()` 并恢复为 `UNDEF`,让 phpy 自己的 Zend object destructor 处理 Python reference 和 GIL。 |
||||||
|
- import 失败时 slot 保持 `UNDEF`,不能缓存异常值或半初始化对象。 |
||||||
|
|
||||||
|
清理由 TypePHP 使用普通 Zend zval API 完成,不调用 phpy C++ 符号: |
||||||
|
|
||||||
|
```cpp |
||||||
|
for (zval &module : php_python_module_map) { |
||||||
|
if (!Z_ISUNDEF(module)) { |
||||||
|
zval_ptr_dtor(&module); |
||||||
|
ZVAL_UNDEF(&module); |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
TypePHP 只释放 Zend object;其内部 Python 引用计数、GIL 和 error state 仍由 phpy object handler 负责。 |
||||||
|
|
||||||
|
### 6.2 `sys.modules` 仍是全局事实来源 |
||||||
|
|
||||||
|
Python import 本身就是全局的。getter 首次调用底层 import 时,CPython 从 `sys.modules` 返回已加载 module 或执行首次加载。`pythonModuleMap` 不是第二套 import 系统,只相当于 Python 文件执行 `import numpy as np` 后保存在该文件 namespace 中的绑定: |
||||||
|
|
||||||
|
```text |
||||||
|
php_get_python_module(id, "numpy") |
||||||
|
-> TypePHP request 内的 PyModule zval binding |
||||||
|
-> CPython sys.modules(全局 module identity 与加载状态) |
||||||
|
``` |
||||||
|
|
||||||
|
它避免每次函数调用都重复进入 Python import API,同时不承担包查找、加载或 reload 逻辑。即使同一个 module 被多个 TypePHP 文件以不同别名引用,只要完整 module 名称相同,就使用同一个 ID 和 `PyModule` Zend object zval。 |
||||||
|
|
||||||
|
该绑定与 Python 普通 import 一致:Python 代码之后删除或替换 `sys.modules['numpy']`,不会自动改变已经完成的 `np` 绑定;显式执行 `PyCore::import('numpy')` 则按调用当时的 `sys.modules` 状态处理。 |
||||||
|
|
||||||
|
规则: |
||||||
|
|
||||||
|
- `python` 根命名空间的大小写不敏感,`python`、`Python`、`PYTHON` 均识别为同一个语言符号。 |
||||||
|
- 只有根命名空间不区分大小写。后续模块路径、成员、方法和关键字参数名称严格区分大小写。 |
||||||
|
- 全局 namespace 中的 `python\package\module\member`,以及其他 namespace 中的 `\python\package\module\member`,是完整 module 访问,不需要 `use`,并按首次实际执行进行 lazy import。 |
||||||
|
- namespace 内没有前导 `\` 的 `python\...` 是相对 PHP 名称,必须按 PHP 规则加上当前 namespace,不能识别为 Python module。 |
||||||
|
- `use python\...` 只能导入 Python 模块。 |
||||||
|
- 是否存在该模块只能在运行时由 CPython 判断。 |
||||||
|
- 不支持 `from package import *`。 |
||||||
|
- 初版不设计单独的 `from package import name` 语法,成员统一通过模块别名访问。 |
||||||
|
- 根命名空间 `\python` 保留给语言互调用;例如 `App\python` 仍是普通 PHP namespace。 |
||||||
|
- 模块别名不能与当前文件中的 TypePHP 类、命名空间导入或其他 Python 模块别名冲突。 |
||||||
|
- 用户仍可直接调用 `PyCore::import()` 并把返回的 `PyModule` 保存到普通变量;完整名称和经 PHP 普通 `use` 解析后的名称都使用 `pythonModuleMap` lazy binding。 |
||||||
|
|
||||||
|
示例: |
||||||
|
|
||||||
|
```php |
||||||
|
python\len($value); // 正确 |
||||||
|
Python\len($value); // 正确,根命名空间大小写不同 |
||||||
|
python\Len($value); // 错误,Python builtin 名称大小写错误 |
||||||
|
Python\Len($value); // 错误 |
||||||
|
``` |
||||||
|
|
||||||
|
解析结果位于根命名空间 `\python` 时,它由 TypePHP 编译器转换为 Python 语言符号;解析为 `App\python` 等其他名称时,仍进行普通 PHP 函数或类查找。 |
||||||
|
|
||||||
|
## 7. 模块成员 |
||||||
|
|
||||||
|
Python module 在 TypePHP 中表现为 namespace,而不是 class。模块中的名称仍由 Python VM 作为属性动态解析。 |
||||||
|
|
||||||
|
### 7.1 包变量 |
||||||
|
|
||||||
|
读取 Python 包变量使用 PHP namespace constant 的语法形式 `module\name`: |
||||||
|
|
||||||
|
```php |
||||||
|
use python\math; |
||||||
|
use python\os; |
||||||
|
use python\numpy as np; |
||||||
|
|
||||||
|
$pi = math\pi; |
||||||
|
$environ = os\environ; |
||||||
|
$arrayType = np\ndarray; |
||||||
|
$directPi = python\math\pi; |
||||||
|
$text = math\pi->__str__(); |
||||||
|
``` |
||||||
|
|
||||||
|
这里使用的是 PHP 合法的 namespace constant 表达式,但 TypePHP 不会把它注册为 Zend constant,也不会进行常量折叠。编译器将每次读取 lowering 为 Python module attribute lookup,结果保持为 `PyObject`,因此可以继续调用对象方法。 |
||||||
|
|
||||||
|
不允许使用 `math::pi` 或 `math::$pi` 读取包变量;两者都是 class member 语法,会错误地把 module 表达为 class。编译器发现这类旧语法时给出有针对性的 FatalError,并提示改用 `math\pi`。 |
||||||
|
|
||||||
|
### 7.2 包函数和类构造 |
||||||
|
|
||||||
|
调用 Python 包中的 callable 使用 PHP namespace function 语法 `module\name(...)`: |
||||||
|
|
||||||
|
```php |
||||||
|
$a = np\array([1, 2, 3]); |
||||||
|
$b = np\array([4, 5, 6]); |
||||||
|
$c = np\add($a, $b); |
||||||
|
$root = python\math\sqrt(16); |
||||||
|
$joined = python\os\path\join('/tmp', 'file.txt'); |
||||||
|
``` |
||||||
|
|
||||||
|
编译器读取 module 的 `name` 属性,并调用得到的 Python 对象。该对象可以是: |
||||||
|
|
||||||
|
- Python 函数。 |
||||||
|
- Python class,此时调用执行该类的构造过程并返回实例。 |
||||||
|
- 实现 `__call__` 的其他 Python 对象。 |
||||||
|
|
||||||
|
TypePHP 不需要也不能仅根据 `np\array()` 的语法判断它是函数还是类构造;可调用性由 Python 在运行时判断。成员不存在时产生 Python `AttributeError`,成员不可调用时产生 Python `TypeError`,并统一映射为 `PyError`。 |
||||||
|
|
||||||
|
初版只支持读取模块属性。PHP namespace constant 语法本身不能作为赋值目标;需要写入时应通过 Python 对象 API 显式完成: |
||||||
|
|
||||||
|
```php |
||||||
|
$os = PyCore::import('os'); |
||||||
|
python\setattr($os, 'name', $value); |
||||||
|
``` |
||||||
|
|
||||||
|
## 8. Python 内置函数与 phpy 语法糖 |
||||||
|
|
||||||
|
`python\name()` 表示调用 Python builtins: |
||||||
|
|
||||||
|
```php |
||||||
|
python\print('hello'); // 等价于 PyCore::print('hello') |
||||||
|
$length = python\len($value)->toValue()->toInt(); |
||||||
|
$range = python\range(0, 10); |
||||||
|
$type = python\type($value); |
||||||
|
``` |
||||||
|
|
||||||
|
它不是普通 TypePHP 命名空间函数。编译器使用 class/func map 解析 `PyCore` 对应的 `zend_function*` 并动态调用,运行时语义与直接编写对应 `PyCore` 调用一致。 |
||||||
|
|
||||||
|
名称严格区分大小写。对于编译器内建映射中已知的错误名称,可以在编译期报错;其他动态 builtin lookup 失败时产生 Python `AttributeError`。 |
||||||
|
|
||||||
|
一部分名称是现有 phpy 类型构造器的语法糖,而不是直接调用同名 Python builtin: |
||||||
|
|
||||||
|
| TypePHP 语法 | 等价 phpy API | |
||||||
|
|---|---| |
||||||
|
| `python\dict($array)` | `new PyDict($array)` | |
||||||
|
| `python\list($array)` | `new PyList($array)` | |
||||||
|
| `python\tuple($array)` | `new PyTuple($array)` | |
||||||
|
| `python\set($array)` | `new PySet($array)` | |
||||||
|
| `python\str($value)` | `new PyStr($value)` | |
||||||
|
| `python\object($value)` | `new PyObject($value)` | |
||||||
|
| `python\print(...)` | `PyCore::print(...)` | |
||||||
|
| `python\scalar($value)` | `PyCore::scalar($value)` | |
||||||
|
|
||||||
|
例如: |
||||||
|
|
||||||
|
```php |
||||||
|
$dict1 = new PyDict([1, 2, 3, 4]); |
||||||
|
$dict2 = python\dict([1, 2, 3, 4]); |
||||||
|
``` |
||||||
|
|
||||||
|
二者必须具有完全相同的运行时语义。这里不能简单转发 CPython `dict([1, 2, 3, 4])`,因为原生 Python builtin 会把参数解释为 key/value pair iterable,与 `PyDict` 的 PHP array 构造规则不同。 |
||||||
|
|
||||||
|
所有语法糖的映射必须形成封闭、经过测试的表,不能仅凭函数名猜测。 |
||||||
|
|
||||||
|
该映射同时决定编译期静态类型: |
||||||
|
|
||||||
|
```php |
||||||
|
$list1 = new PyList(); |
||||||
|
$list2 = python\list(); |
||||||
|
|
||||||
|
$dict1 = new PyDict(); |
||||||
|
$dict2 = python\dict(); |
||||||
|
``` |
||||||
|
|
||||||
|
- `$list1` 与 `$list2` 都是 `PyList` typed object。 |
||||||
|
- `$dict1` 与 `$dict2` 都是 `PyDict` typed object。 |
||||||
|
- 两种写法必须使用相同的类型检查、方法解析和 Native Call 优化。 |
||||||
|
- 语法糖不能退化成 `mixed`、`var` 或只有基础类型 `PyObject`。 |
||||||
|
- Python builtin 调用同样遵守对象保持规则,例如 `python\len()` 返回包装 Python int 的 `PyObject`;需要先以 `toValue()`(或函数入口 `python\scalar()`)离开 Python 对象规则,再使用普通 TypePHP 转换得到确定类型。`python\print()` 的 Python `None` 结果也保持为 `PyObject`,作为独立语句使用时可直接丢弃。 |
||||||
|
- `PyObject::toValue()` 和 `python\scalar()` 都不是普通 Python builtin 调用,而是明确要求退出 Python 类型规则的转换边界,因此返回 TypePHP `var`。 |
||||||
|
- 动态 Python module 成员调用统一返回 `PyObject`。 |
||||||
|
|
||||||
|
## 9. Python 对象类型 |
||||||
|
|
||||||
|
所有无法在编译期确定静态类型的 Python 值统一表示为: |
||||||
|
|
||||||
|
```php |
||||||
|
PyObject |
||||||
|
``` |
||||||
|
|
||||||
|
`PyObject` 是现有 phpy 的公开类型,也是 TypePHP 的正式运行时类型。不会再引入 `python\Object` 或 `python\Any`。 |
||||||
|
|
||||||
|
Python 内建类型继续使用 phpy 已有的具体代理类,例如 `PyDict`、`PyList`、`PyTuple`、`PySet`、`PyStr`、`PyType`、`PyFn` 和 `PyIter`。这样普通 PHP 与 TypePHP 用户看到的是同一套类型体系。 |
||||||
|
|
||||||
|
Python 的 `None` 也是一个合法 Python 对象。它与 TypePHP `null` 的自动转换规则需要单独定义,不能通过空指针表示 Python `None`。 |
||||||
|
|
||||||
|
## 10. 对象操作 |
||||||
|
|
||||||
|
### 10.1 属性和方法 |
||||||
|
|
||||||
|
```php |
||||||
|
$env = os\environ; |
||||||
|
$items = $env->items(); |
||||||
|
$name = $object->name; |
||||||
|
$object->name = 'new value'; |
||||||
|
unset($object->name); |
||||||
|
``` |
||||||
|
|
||||||
|
分别映射为 Python 的 `getattr`、call、`setattr` 和 `delattr` 协议。 |
||||||
|
|
||||||
|
`PyObject` 明确提供 `toValue()` 和 `toArray()` 两个 PHP Facade 方法。`toValue()` 等价于 `PyCore::scalar()` / `python\scalar()`,把 Python 值递归转换为 PHP 内置值。其返回值再使用普通 TypePHP 转换方法确定类型: |
||||||
|
|
||||||
|
```php |
||||||
|
$pyValue = np\int64(42); // PyObject |
||||||
|
$value = $pyValue->toValue()->toInt(); // TypePHP int |
||||||
|
``` |
||||||
|
|
||||||
|
这里的 `toInt()` 作用于 `toValue()` 已返回的 TypePHP 值,并非作用于 `PyObject`。 |
||||||
|
|
||||||
|
`toArray()` 仅转换 Python `list`、`tuple`、`set`、`dict` 以及 iterator。容器元素递归转换为 PHP 值;iterator 会被消费,后续再次转换只能得到其剩余元素。不支持转换的 Python 类型返回空数组。`toArray()` 同时是 TypePHP 关键词方法,但 PHPX 的对象转换路径会调用 `PyObject::toArray()`;`toString()` 则继续通过关键词方法调用 `PyObject::__toString()`,phpy 不重复声明 `toString()`。 |
||||||
|
|
||||||
|
### 10.2 下标 |
||||||
|
|
||||||
|
```php |
||||||
|
$value = $object[$key]; |
||||||
|
$object[$key] = $value; |
||||||
|
unset($object[$key]); |
||||||
|
isset($object[$key]); |
||||||
|
``` |
||||||
|
|
||||||
|
分别映射到 Python mapping/sequence protocol。 |
||||||
|
|
||||||
|
`isset()` 保持 PHP 的空值语义:键或索引不存在时返回 `false`,对应值为 Python `None` 时也返回 `false`。运行时只把 `KeyError` / `IndexError` 识别为“缺失”;Python protocol 抛出的其他异常必须继续映射为 `PyError`,不得被 `isset()` 吞掉。list 和 tuple 的整数下标遵循 Python 负索引规则。 |
||||||
|
|
||||||
|
### 10.3 调用对象 |
||||||
|
|
||||||
|
```php |
||||||
|
$result = $callable($arg1, $arg2); |
||||||
|
``` |
||||||
|
|
||||||
|
运行时使用 `PyObject_Call`。不可调用对象产生 Python `TypeError`,并映射为 TypePHP 可捕获的 Python 异常。 |
||||||
|
|
||||||
|
### 10.4 迭代 |
||||||
|
|
||||||
|
```php |
||||||
|
foreach ($pythonIterable as $value) { |
||||||
|
// Python __iter__ / __next__ |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
带 key 的形式: |
||||||
|
|
||||||
|
```php |
||||||
|
foreach ($pythonIterable as $index => $value) { |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
通用 Python iterator 使用从 `0` 开始的 TypePHP 迭代序号作为 `$index`,`$value` 是 `__next__()` 产出的对象。`PyDict` 是 phpy 的专用 mapping wrapper,带 key 的 `foreach` 使用 PHP mapping 习惯:`$index` 是 dict key,`$value` 是对应 dict value。`__iter__()` / `__next__()` 的 Python 异常必须传播为 `PyError`,不能当作正常迭代结束。 |
||||||
|
|
||||||
|
## 11. 参数与关键字参数 |
||||||
|
|
||||||
|
普通参数按从左到右顺序求值,然后构造 Python positional args: |
||||||
|
|
||||||
|
```php |
||||||
|
$model = AutoModel\from_pretrained( |
||||||
|
'model-name', |
||||||
|
trust_remote_code: true, |
||||||
|
device_map: 'auto', |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
TypePHP 命名参数映射为 Python keyword arguments。参数名严格区分大小写。 |
||||||
|
|
||||||
|
PHP/TypePHP 数组展开规则可用于构造位置参数和关键字参数,但必须满足: |
||||||
|
|
||||||
|
- 整数 key 生成 positional argument。 |
||||||
|
- 字符串 key 生成 keyword argument。 |
||||||
|
- positional argument 不能出现在 keyword argument 之后。 |
||||||
|
- 重复 keyword 产生 Python `TypeError`。 |
||||||
|
|
||||||
|
是否增加显式的 `python\args()` / `python\kwargs()` 类型,留待后续讨论;初版尽量复用现有调用和数组展开语法。 |
||||||
|
|
||||||
|
## 12. 显式转换原则 |
||||||
|
|
||||||
|
TypePHP 不继承 phpy 在 ZendVM Facade/opcode 层面的返回值隐式转换行为。语言层采用“参数进入 Python 边界时自动转换、Python 返回值保持对象、返回 TypePHP 时显式转换”的原则。 |
||||||
|
|
||||||
|
允许自动转换的场景必须由语法明确指出正在进入 Python: |
||||||
|
|
||||||
|
- `python\name(...)`。 |
||||||
|
- Python module 调用,例如 `np\array(...)`。 |
||||||
|
- `PyObject` 的方法或 callable 调用。 |
||||||
|
- 显式 Python 容器构造,例如 `new PyList(...)` 或 `python\list(...)`。 |
||||||
|
- 参数声明要求 `PyObject`、`PyDict` 等 phpy 类型。 |
||||||
|
- `PyObject` 与 TypePHP 值组成的混合运算表达式。 |
||||||
|
|
||||||
|
在这些调用边界内,所有参数表达式先严格按照 TypePHP 从左到右的顺序求值,再转换为 Python 能接受的对象。TypePHP 标量转换为对应 Python scalar;TypePHP 数组递归转换为 Python list/dict,这一过程会产生深拷贝。这不应扩散为不含 Python 对象的普通 TypePHP 表达式中的全局隐式转换。 |
||||||
|
|
||||||
|
“所有参数自动转换”只适用于转换表明确支持的 TypePHP 类型;resource 或其他没有 Python 表示形式的值必须抛出清晰的类型错误,不能静默转换或传递无效指针。 |
||||||
|
|
||||||
|
以下场景不允许隐式转换: |
||||||
|
|
||||||
|
- 将 `PyObject` 直接赋给 `int`、`float`、`bool`、`string` 或 `array`。 |
||||||
|
- 将 Python 容器隐式深拷贝成 TypePHP array。 |
||||||
|
- 因算术、比较或字符串上下文而擅自把 Python 对象变成 TypePHP 标量。 |
||||||
|
- 根据运行时 Python 类型改变 TypePHP 变量的静态类型。 |
||||||
|
|
||||||
|
`echo $pyObject` 可继续兼容现有 `PyObject::__toString()`,但这只属于输出协议,不能被编译器当作一般的字符串隐式转换。 |
||||||
|
|
||||||
|
## 13. TypePHP 到 Python 的转换 |
||||||
|
|
||||||
|
Python 调用边界允许以下自动转换: |
||||||
|
|
||||||
|
| TypePHP | Python | 语义 | |
||||||
|
|---|---|---| |
||||||
|
| `null` | `None` | 单例,不是空 `PyObject*` | |
||||||
|
| `bool` | `bool` | 值转换 | |
||||||
|
| `int` | `int` | Python 任意精度整数 | |
||||||
|
| `float` | `float` | double | |
||||||
|
| `string` | `str` | 要求合法 UTF-8 | |
||||||
|
| list array | `list` | 递归复制 | |
||||||
|
| map array | `dict` | 递归复制 | |
||||||
|
| `PyObject` 及其子类 | 原对象 | 零拷贝,只传递引用 | |
||||||
|
| TypePHP callable | Python callable proxy | Python 可同步回调 TypePHP | |
||||||
|
| TypePHP object | Zend object proxy | 不自动复制对象属性 | |
||||||
|
|
||||||
|
PHP array 使用 `zend_array_is_list()` 一类规则决定转换为 Python `list` 还是 `dict`。空数组默认转换为 Python `list`;如需空 dict,必须提供显式构造 API。 |
||||||
|
|
||||||
|
数组和普通 TypePHP 字符串每次进入 Python 边界都可能产生分配与复制。文档和性能诊断应建议高频调用、循环调用或大数据场景尽早构造并复用 `PyDict`、`PyList`、`PyStr` 等原生 Python 代理类型,避免重复深拷贝。`PyObject` 及其子类进入 Python 边界时只传递原对象引用,不做内容复制。 |
||||||
|
|
||||||
|
推荐写法: |
||||||
|
|
||||||
|
```php |
||||||
|
// 只转换一次,后续调用传递同一个 Python 对象。 |
||||||
|
use python\processor; |
||||||
|
|
||||||
|
$pyItems = python\list($items); |
||||||
|
for ($i = 0; $i < 1000; $i++) { |
||||||
|
processor\consume($pyItems); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
应避免在循环中反复把同一个 TypePHP 容器作为参数传入,因为每次跨越 Python 调用边界都会重新深拷贝: |
||||||
|
|
||||||
|
```php |
||||||
|
for ($i = 0; $i < 1000; $i++) { |
||||||
|
processor\consume($items); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
字符串与 bytes 必须区分。TypePHP `string` 默认映射到 Python `str`;二进制内容使用显式 `python\bytes()`。 |
||||||
|
|
||||||
|
递归数组、循环引用和超深嵌套必须检测并抛出异常,不能无限递归。 |
||||||
|
|
||||||
|
## 14. Python 到 TypePHP 的转换 |
||||||
|
|
||||||
|
### 14.1 默认规则 |
||||||
|
|
||||||
|
TypePHP 的 Python 专用调用路径必须关闭 phpy 的返回值隐式转换,所有 Python 函数、方法、构造调用和运算结果均保持为 phpy 对象。动态调用的静态返回类型统一为 `PyObject`,不能因为运行时结果恰好是 Python `bool`、`int`、`float`、`str`、`list` 或 `dict` 就隐式转换为 TypePHP 值。 |
||||||
|
|
||||||
|
当前实现由生成代码在首次实际执行 Python 表达式时,动态调用 `PyCore::setOptions(['return_as_object' => true])`。该初始化是请求级 lazy guard:只写 `use python\module` 而不访问 Python 符号不会触发 phpy;constructor-only 程序也会在构造前完成配置;request clean 会重置 TypePHP 自身的 guard。后续若 phpy 提供无全局模式的对象保持型独立入口,可在不改变语言语义的前提下替换这一运行时实现。 |
||||||
|
|
||||||
|
编译器已知的 phpy 构造语法糖仍保留精确子类,例如 `python\list()` 返回 `PyList`、`python\dict()` 返回 `PyDict`;这些类型本身都是 `PyObject` 子类,不构成返回值隐式转换。 |
||||||
|
|
||||||
|
phpy Zend Facade 应提供相互独立的“保持 Python 对象”和“显式转换为 TypePHP”入口。不能通过修改进程级全局函数指针或全局转换模式来临时切换,否则嵌套调用、同步重入和异常路径可能把错误策略泄漏给后续调用。TypePHP 生成的普通 Python 调用只动态调用对象保持入口;`PyObject::toValue()` 与 `python\scalar()` 最终都调用明确的标量转换入口。 |
||||||
|
|
||||||
|
phpy 内部已使用 `PythonToPhpConverter` 与 `PhpToPythonConverter` 实现这一约束。每次顶层转换拥有独立实例,递归子值复用同一实例;容器进入与退出由 RAII guard 管理,循环容器和超过深度限制的输入会抛出 `PyError`,不会污染后续转换或导致进程崩溃。 |
||||||
|
|
||||||
|
原因: |
||||||
|
|
||||||
|
- 保留 Python 对象身份和精确类型。 |
||||||
|
- 避免容器返回时立即深拷贝。 |
||||||
|
- Python `int` 可能超过 TypePHP `int` 范围。 |
||||||
|
- Python 类型的子类可能重载协议,不能按基础容器强制展开。 |
||||||
|
- 避免 phpy 当前“部分标量自动转换、部分对象保留包装”的行为进入 TypePHP 静态类型系统。 |
||||||
|
|
||||||
|
### 14.2 显式转换 |
||||||
|
|
||||||
|
Python 对象只有通过 `toValue()`、`python\scalar()`(或手写等价的 `PyCore::scalar()`)才能进入 TypePHP 类型规则: |
||||||
|
|
||||||
|
```php |
||||||
|
$nativeValue1 = PyCore::scalar($value); |
||||||
|
$nativeValue2 = python\scalar($value); // 完全等价的语法糖 |
||||||
|
$nativeValue3 = $value->toValue(); |
||||||
|
$integer = $value->toValue()->toInt(); |
||||||
|
$float = $value->toValue()->toFloat(); |
||||||
|
$boolean = $value->toValue()->toBool(); |
||||||
|
$string = $value->toValue()->toString(); |
||||||
|
$array = $value->toArray(); |
||||||
|
``` |
||||||
|
|
||||||
|
规则: |
||||||
|
|
||||||
|
- `toValue()` 是 `PyObject` 的普通公开方法,不注册为 TypePHP 关键词方法;它在 phpy 内部复用与 `PyCore::scalar()` 相同的转换器。 |
||||||
|
- `toArray()` 保留 TypePHP 全局关键词方法语义。PHPX 对对象执行数组转换时优先调用其公开的 `toArray()`,因此会进入 phpy 实现。 |
||||||
|
- 显式转换完成后,结果完全进入 TypePHP 的静态类型、运算符和参数传递规则,不再采用 Python protocol。 |
||||||
|
- 容器转换属于显式深转换,并检测递归引用。 |
||||||
|
- Python 大整数不能静默溢出;现有转换规则需要 review 后再确定与 TypePHP `BigInt` 的精确映射。 |
||||||
|
- Python `str` 与 `bytes` 必须区分,不能都无条件转换为 TypePHP string。 |
||||||
|
- phpy 负责 `PyObject::toValue()` / `PyCore::scalar()` 的通用值转换,以及受限的 `PyObject::toArray()` 容器转换。`toInt/toFloat/toBool` 属于转换后的 PHP 值;`toString()` 仍由 TypePHP 关键词方法调用 `PyObject::__toString()`。 |
||||||
|
|
||||||
|
现有 phpy 的 PHP 用户仍可保留兼容行为;TypePHP 调用 phpy 的对象保持型 Zend API。为此可以重构或新增 phpy internal class method,但不增加 TypePHP 到 phpy 的 C++ 链接依赖。 |
||||||
|
|
||||||
|
## 15. 通过 Python `operator` module 实现运算符 |
||||||
|
|
||||||
|
对于 `PyObject` 及其子类: |
||||||
|
|
||||||
|
- `+ - * / % ** << >> & | ^` 映射为 Python 标准库 `operator` module 的对应函数。 |
||||||
|
- `/` 映射 `operator.truediv()`,不能映射 `operator.floordiv()`。 |
||||||
|
- Python floor division 暂用 `python\floordiv($a, $b)`,因为 TypePHP 没有 `//` 运算符。 |
||||||
|
- `== != < <= > >=` 分别映射 `operator.eq/ne/lt/le/gt/ge()`。 |
||||||
|
- `===` / `!==` 分别映射 `operator.is_()` / `operator.is_not()`。 |
||||||
|
- `if ($object)`、`!$object` 使用 `operator.truth()`。 |
||||||
|
- compound assignment 映射 `operator.iadd/isub/...()`,并用返回对象更新左值。 |
||||||
|
|
||||||
|
基础映射: |
||||||
|
|
||||||
|
| TypePHP | 生成的动态调用 | |
||||||
|
|---|---| |
||||||
|
| `$a + $b` | `operator\add($a, $b)` | |
||||||
|
| `$a - $b` | `operator\sub($a, $b)` | |
||||||
|
| `$a * $b` | `operator\mul($a, $b)` | |
||||||
|
| `$a / $b` | `operator\truediv($a, $b)` | |
||||||
|
| `$a % $b` | `operator\mod($a, $b)` | |
||||||
|
| `$a ** $b` | `operator\pow($a, $b)` | |
||||||
|
| `$a << $b` | `operator\lshift($a, $b)` | |
||||||
|
| `$a >> $b` | `operator\rshift($a, $b)` | |
||||||
|
| `$a & $b` | `operator\and_($a, $b)` | |
||||||
|
| bitwise OR | `operator\or_($a, $b)` | |
||||||
|
| `$a ^ $b` | `operator\xor($a, $b)` | |
||||||
|
| `-$a` | `operator\neg($a)` | |
||||||
|
| `+$a` | `operator\pos($a)` | |
||||||
|
| `~$a` | `operator\invert($a)` | |
||||||
|
| `$a += $b` | `$a = operator\iadd($a, $b)` | |
||||||
|
|
||||||
|
所有操作数必须严格从左到右求值。 |
||||||
|
|
||||||
|
即使源码没有显式写出 `use python\operator`,出现 Python 运算符时,编译器也将其视为一个仅供内部 lowering 使用的隐式 module binding,并通过同一 `pythonModuleMap` 取得 `operator` module。它不向用户文件注入可见别名,因此不会与用户自己定义的 `operator` class 或 use alias 冲突。用户显式 `use python\operator` 时,内部 lowering 和用户访问复用同一个 module ID。 |
||||||
|
|
||||||
|
identity 比较调用 `operator\is_()` / `operator\is_not()`。即使两个对象的 `operator\eq()` 结果为真,只要不是同一个 Python object,`===` 仍为假。 |
||||||
|
|
||||||
|
允许 Python 对象与 TypePHP 值直接混合运算。只要当前运算节点的一侧静态类型为 `PyObject` 或其子类,另一侧的 TypePHP 表达式先完整地按 TypePHP 规则求值,再把所得值转换为 Python 对象,最后由 CPython 执行当前运算节点对应的 protocol。 |
||||||
|
|
||||||
|
例如: |
||||||
|
|
||||||
|
```php |
||||||
|
$result1 = $pyInt + 10; // 10 转为 Python int,由 Python 执行加法 |
||||||
|
$result2 = $pyList * getCount(); // 先求值 getCount(),再转为 Python int |
||||||
|
$native = $pyInt->toValue()->toInt() + 10; // 已显式转为 TypePHP int,使用 TypePHP 加法 |
||||||
|
``` |
||||||
|
|
||||||
|
`operator` 调用结果仍为 `PyObject`,以保留 Python 自定义运算符可能返回的任意对象。`===` / `!==` 和条件分支是例外:`operator.is_/is_not/truth()` 的 Python bool 结果随后通过显式 phpy 转换入口得到 TypePHP `bool`。两侧操作数必须严格从左到右各求值一次,转换过程不得导致表达式重复执行。 |
||||||
|
|
||||||
|
phpy 作为普通 PHP 扩展时,可以继续使用 Zend opcode handler 提供运算符重载兼容性;TypePHP 不依赖这些 handler。 |
||||||
|
|
||||||
|
动态 ZendVM 代码存在一个明确保留的限制:Zend 会把 `-$value` / `+$value` 编译为乘以 `-1` / `1`,phpy 的 opcode handler 无法再识别源码中的一元运算。因此动态代码保持 `$value * -1` / `$value * 1` 的协议行为,不通过全局 AST hook 改写普通 PHP 代码;自定义 Python 对象的 `__neg__()` / `__pos__()` 与 `__mul__()` 不一致时,结果可能不同。TypePHP AOT 仍按照上表生成 `operator.neg()` / `operator.pos()`。外部用户文档 `python.md` 已明确说明这一限制。 |
||||||
|
|
||||||
|
TypePHP 编译器在识别到静态类型为 `PyObject`、`PyDict` 等 phpy 对象时,把运算符改写为普通 Python module callable 调用: |
||||||
|
|
||||||
|
```text |
||||||
|
TypePHP operator |
||||||
|
-> compile-time lowering |
||||||
|
-> implicit python\operator module binding |
||||||
|
-> operator\add/sub/... dynamic call |
||||||
|
-> CPython complete operator protocol |
||||||
|
``` |
||||||
|
|
||||||
|
该抽象不直接链接 phpy,也不经过 phpy 的 user opcode handler,但它不是无调用成本的 C++ inline 操作: |
||||||
|
|
||||||
|
- `zend_function*` 和 class entry 使用现有 func/class map lazy cache。 |
||||||
|
- 参数仍需要构造为 Zend values,并由 phpy 转为 Python 对象。 |
||||||
|
- Python module member lookup、GIL、CPython call 和引用计数成本仍然存在。 |
||||||
|
- 优点是 TypePHP 二进制只依赖 ZendVM/PHPX,phpy 可以作为真正的可选运行时扩展。 |
||||||
|
|
||||||
|
使用标准库 `operator.add()` 而不是直接调用 `__add__()`,可以复用 CPython 对 `NotImplemented`、`__radd__()`、右操作数子类优先级等完整规则,TypePHP 不实现 reflected-operation fallback。 |
||||||
|
|
||||||
|
当前实现已经覆盖二元算术和位运算、比较、identity、一元运算、条件真假值、短路逻辑,以及 variable、属性和下标左值的复合赋值。Python module function/property、builtin、动态方法、属性、下标和 callable 的结果都会继续传播 `PyObject` 静态类型,因此可以直接链式访问或参与后续 Python 运算。 |
||||||
|
|
||||||
|
## 16. 异常 |
||||||
|
|
||||||
|
Python 调用失败时抛出统一的 TypePHP 异常类型,暂定: |
||||||
|
|
||||||
|
```php |
||||||
|
PyError |
||||||
|
``` |
||||||
|
|
||||||
|
异常至少保留: |
||||||
|
|
||||||
|
- Python exception type。 |
||||||
|
- message。 |
||||||
|
- Python traceback 对象。 |
||||||
|
- 格式化后的 traceback 字符串。 |
||||||
|
- 原始 Python exception instance。 |
||||||
|
|
||||||
|
示例: |
||||||
|
|
||||||
|
```php |
||||||
|
try { |
||||||
|
np\array('invalid')->reshape(2, 2); |
||||||
|
} catch (PyError $error) { |
||||||
|
echo $error->pythonType(); |
||||||
|
echo $error->pythonTraceback(); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Python 同步调用 TypePHP callable 代理时,如果 TypePHP 抛出异常,应转换为普通 Python 异常,并保留原始 TypePHP 类名和消息。该异常只沿当前动态调用栈传播,不要求注册 `typephp` Python module 或专用的全局异常类型。 |
||||||
|
|
||||||
|
异常跨 VM 后必须清理源 VM 的 pending exception 状态。任何异常转换失败都不能导致 coredump、重复抛出或遗留错误状态。 |
||||||
|
|
||||||
|
## 17. TypePHP callable 传给 Python |
||||||
|
|
||||||
|
TypePHP 函数、闭包和可调用对象可以自动包装为 Python callable: |
||||||
|
|
||||||
|
```php |
||||||
|
$values = python\list([1, 2, 3]); |
||||||
|
$result = python\map(fn (int $value): int => $value * 2, $values); |
||||||
|
``` |
||||||
|
|
||||||
|
Python 调用代理时: |
||||||
|
|
||||||
|
1. Python 参数按边界规则转换或包装为 TypePHP 值。 |
||||||
|
2. 进入 ZendVM 调用 callable。 |
||||||
|
3. 返回值转换为 Python 值。 |
||||||
|
4. TypePHP 异常转换为 Python 异常。 |
||||||
|
|
||||||
|
闭包代理必须持有 Zend callable,防止 callable 在 Python 仍引用它时被释放。跨 VM 引用环必须由运行时显式检测或提供可预测的回收策略。 |
||||||
|
|
||||||
|
TypePHP callable 代理只是参数值,不是导出机制:只有 TypePHP 主动把代理传给 Python 后,Python 才能在该对象存活期间动态调用它。TypePHP 不生成可供 Python 独立导入的 module,也不注册全局函数或类。 |
||||||
|
|
||||||
|
## 18. phpy 生命周期与集成方式 |
||||||
|
|
||||||
|
TypePHP 复用 phpy 自己的 PHP 扩展入口和生命周期,不增加独立的 CPython bootstrap: |
||||||
|
|
||||||
|
1. phpy 的 `MINIT` 初始化共享运行时、CPython 以及 `PyObject`、`PyDict` 等 Zend 类。 |
||||||
|
2. phpy 的 `RINIT` 建立本次请求需要的状态。 |
||||||
|
3. TypePHP 程序在请求期间通过 phpy 注册到 ZendVM 的 internal classes、methods 和 object handlers 动态调用 Python。 |
||||||
|
4. phpy 的 `RSHUTDOWN` 释放请求级资源和代理。 |
||||||
|
5. phpy 的 `MSHUTDOWN` 在所有代理均已安全释放后关闭共享运行时和 CPython。 |
||||||
|
|
||||||
|
TypePHP 应通过与其他静态或动态链接 PHP 扩展相同的机制执行这些入口,不能重复初始化 CPython,也不能绕过 phpy 生命周期直接调用 `Py_Initialize()` 或 `Py_Finalize()`。 |
||||||
|
|
||||||
|
唯一产物是以 TypePHP 为入口的主程序或库。不会生成可被 CPython 导入的 `.so` / `.pyd`,不会向 Python 注册 TypePHP module、函数或类,也不存在 `#[PythonExport]`。 |
||||||
|
|
||||||
|
## 19. 性能原则 |
||||||
|
|
||||||
|
- `PyObject` 传参只增加必要的引用计数,不复制 Python 对象。 |
||||||
|
- `pythonModuleMap` 只缓存已经绑定的 `PyModule` Zend object zval,真实加载和全局 identity 直接复用 CPython `sys.modules`;builtin/member lookup 初版保持简单,只有基准测试证明必要时才单独设计缓存。 |
||||||
|
- 参数应直接构造 vectorcall 所需数组,优先使用 CPython vectorcall API。 |
||||||
|
- 避免先构造 PHP 数组,再由 phpy 二次转换为 Python tuple/dict。 |
||||||
|
- TypePHP 数组到 Python 容器属于显式 O(n) 转换,不宣称零成本。 |
||||||
|
- 对进入热点 Python 调用的 TypePHP 数组和字符串,应提升为可复用的 `PyList`、`PyDict`、`PyStr`;编译器不擅自缓存转换结果,因为原 TypePHP 值可能已经改变。 |
||||||
|
- GIL guard 应覆盖最小必要区域;单线程同步重入期间必须保持正确的解释器状态。 |
||||||
|
- 异常路径与正常路径必须同等测试引用计数和内存泄漏。 |
||||||
|
|
||||||
|
## 20. 永久边界与不支持能力 |
||||||
|
|
||||||
|
- Python 线程,包括 `threading` 创建线程以及任何从非主线程进入 phpy/TypePHP bridge 的调用。 |
||||||
|
- `asyncio`、Python coroutine、`async`/`await` 及跨语言事件循环调度。 |
||||||
|
- CPython subinterpreter 和 per-interpreter GIL 模式。 |
||||||
|
- Python 作为入口独立加载 TypePHP 程序。 |
||||||
|
- 生成 Python extension 或将 TypePHP 函数、类、对象注册为可导入的 Python module。 |
||||||
|
- 运行时反射生成 TypePHP 静态类型。 |
||||||
|
- 自动导入 `from module import *`。 |
||||||
|
- pickle/serialize Python 对象。 |
||||||
|
- 跨进程传递 `PyObject`。 |
||||||
|
- WASM target 中的 Python 互调用。 |
||||||
|
|
||||||
|
禁止能力必须有明确防线:编译器对能够静态识别的 `threading`、`_thread`、`asyncio` 和 subinterpreter API 给出 FatalError;phpy 记录创建运行时的 owner thread,并拒绝从其他线程进入 ZendVM bridge。动态导入、反射或第三方包不能被编译器完整识别,因此运行时检查不能省略。 |
||||||
|
|
||||||
|
第三方 native package 内部完全封闭、从不进入 CPython API 或 phpy/ZendVM bridge 的计算线程不属于这里的 Python 线程能力;它们对 TypePHP 不可见,也不得产生跨线程回调。 |
||||||
|
|
||||||
|
## 21. TDD 与测试门禁 |
||||||
|
|
||||||
|
本项目的实现和重构必须严格遵循 TDD,顺序不可颠倒: |
||||||
|
|
||||||
|
1. 根据已确认的设计语义编写测试。 |
||||||
|
2. 运行测试,确认它因为目标能力尚未实现或现有 BUG 而失败。 |
||||||
|
3. 编写使该测试通过的最小实现。 |
||||||
|
4. 运行相关测试和完整回归。 |
||||||
|
5. 在测试保护下重构、清理和优化。 |
||||||
|
6. 再次运行完整回归、内存检查和覆盖率检查。 |
||||||
|
|
||||||
|
禁止先完成实现,再补写只能验证当前实现细节的测试。每个 BUG 必须先添加能够稳定复现问题的回归测试。 |
||||||
|
|
||||||
|
### 21.1 三层强制测试 |
||||||
|
|
||||||
|
#### PHPUnit |
||||||
|
|
||||||
|
TypePHP 仓库的 PHPUnit 用于验证编译器自身: |
||||||
|
|
||||||
|
- Python import 和特殊名称解析。 |
||||||
|
- AST、符号表和类型推断。 |
||||||
|
- C++ 代码生成。 |
||||||
|
- 编译期错误和诊断位置。 |
||||||
|
- 永久禁用能力的编译期诊断,以及无 phpy 环境仍能成功生成代码。 |
||||||
|
- 不需要启动 CPython 的边界逻辑。 |
||||||
|
|
||||||
|
phpy 仓库现有 PHPUnit 用于验证 ZendVM/PHP Facade 与共享 Runtime: |
||||||
|
|
||||||
|
- `PyCore`、`PyObject`、`PyDict` 等公开 PHP API。 |
||||||
|
- PHP 值与 Python 对象转换。 |
||||||
|
- Python 异常映射为 `PyError`。 |
||||||
|
- opcode handler 与 TypePHP 使用的 Zend dynamic-call API 具有一致语义。 |
||||||
|
- TypePHP 需要的对象保持型调用路径。 |
||||||
|
- GIL、引用计数、析构和异常路径。 |
||||||
|
|
||||||
|
#### PHPT |
||||||
|
|
||||||
|
用于从 TypePHP 用户视角验证语言和运行时的端到端行为: |
||||||
|
|
||||||
|
- 导入、`module\name` 包变量读取、`module\name()` callable 调用和关键字参数。 |
||||||
|
- `use python\module as alias` 与手写 `$alias = PyCore::import('module')` 的结果、异常和对象 identity 等价。 |
||||||
|
- 多个别名、嵌套模块和跨 `.cc` 重复导入。 |
||||||
|
- 只有 `use python\module` 而未访问任何相关符号时,不生成 helper、不调用 import,也不检查该 Python module 是否存在。 |
||||||
|
- 同一完整 module 名称跨函数、跨 `.cc` 只分配一个 ID,并只在首次访问时调用 import API。 |
||||||
|
- import 失败保持 map slot 为 `UNDEF`;异常被捕获后,下一次访问可以重新尝试。 |
||||||
|
- request clean 对 module zval 逐项执行 `zval_ptr_dtor()` 并恢复为 `UNDEF`,不得直接 `memset` 有效 Zend object。 |
||||||
|
- 删除或替换 `sys.modules` 条目不会改变已经缓存的 TypePHP module binding。 |
||||||
|
- `module::name` / `module::$name` 旧 class member 语法的编译期 FatalError,以及不存在成员和不可调用成员的运行时异常。 |
||||||
|
- 属性、下标、迭代、运算符和 truthiness。 |
||||||
|
- TypePHP 参数到 Python 的转换,以及 Python 返回值的显式转换。 |
||||||
|
- 空 TypePHP 数组默认转换为 Python list,以及数组递归深拷贝、异常中止和重复转换行为。 |
||||||
|
- Python builtin、模块函数、方法和运算结果不会隐式变成 TypePHP 标量。 |
||||||
|
- `$obj->toValue()->toInt()`、`$obj->toArray()`、`python\scalar($obj)->toInt()` 等显式边界及其后的普通 TypePHP 转换恢复静态类型和运算规则。 |
||||||
|
- Python 异常到 TypePHP 异常。 |
||||||
|
- phpy 未加载时首次 Python 调用抛出 PHP `Error`,而仅声明未使用的 Python `use` 不报错。 |
||||||
|
- TypePHP callable 被 Python 回调。 |
||||||
|
- 引用计数、对象析构和重复调用。 |
||||||
|
- 编译后的真实程序输出,而不是只检查生成代码字符串。 |
||||||
|
|
||||||
|
#### pytest |
||||||
|
|
||||||
|
pytest 用于 phpy 自身已有 Python-facing bridge 的回归测试;它不表示 TypePHP 会生成 Python extension。需要验证: |
||||||
|
|
||||||
|
- Python 调用 PHP 函数、对象和 callable。 |
||||||
|
- 同步重入和 phpy module 生命周期。 |
||||||
|
- Python 对 Zend callable/object proxy 的持有、释放和异常映射。 |
||||||
|
- 永久禁止从 Python 线程进入 ZendVM 的防护。 |
||||||
|
|
||||||
|
三层测试不能互相替代。C++/GoogleTest 可以覆盖 phpy 内部的引用计数、RAII 和低层转换,但不能代替 PHPUnit、PHPT 或 pytest。 |
||||||
|
|
||||||
|
### 21.2 每项语义的测试矩阵 |
||||||
|
|
||||||
|
每个已支持能力至少考虑以下维度: |
||||||
|
|
||||||
|
- 正常路径。 |
||||||
|
- 错误类型和错误消息。 |
||||||
|
- 边界值及空值。 |
||||||
|
- Python 子类和动态协议。 |
||||||
|
- TypePHP → Python → TypePHP 重入。 |
||||||
|
- 仅由 TypePHP 发起、经 callable 代理发生的 Python → TypePHP → Python 同步重入。 |
||||||
|
- 正常析构和异常析构。 |
||||||
|
- 重复执行、`sys.modules` identity 和重复 import 不重新执行 module 代码。 |
||||||
|
- Debug、Release 以及支持的平台。 |
||||||
|
|
||||||
|
转换测试必须包含: |
||||||
|
|
||||||
|
- `PHP_INT_MIN/PHP_INT_MAX` 及超出范围的 Python int。 |
||||||
|
- `NaN`、`INF`、`-INF` 和负零。 |
||||||
|
- 空字符串、Unicode、无效 UTF-8、内含 NUL 的 bytes。 |
||||||
|
- 空 list/dict、混合 key、深层容器、递归容器和循环引用。 |
||||||
|
- 同一 Python 对象经多次包装后的 identity。 |
||||||
|
|
||||||
|
### 21.3 内存与稳定性测试 |
||||||
|
|
||||||
|
涉及 `PyObject*` 或 `zval` 所有权的修改,除功能测试外还必须执行: |
||||||
|
|
||||||
|
- PHP memory leak report。 |
||||||
|
- Python debug build/refcount 检查(环境可用时)。 |
||||||
|
- ASan/UBSan 构建。 |
||||||
|
- 异常注入测试,覆盖每一个可能提前返回的分支。 |
||||||
|
- 循环创建和销毁对象的压力测试。 |
||||||
|
- 进程退出时仍存在跨 VM 代理对象的测试。 |
||||||
|
|
||||||
|
不允许把 coredump、泄漏或未清理的 pending exception 标记为“预期行为”来绕过测试。 |
||||||
|
|
||||||
|
### 21.4 覆盖率要求 |
||||||
|
|
||||||
|
- 设计文档中每一条规范性行为都必须能够对应到至少一个测试。 |
||||||
|
- 新增和修改的桥接代码需要覆盖正常分支与错误分支。 |
||||||
|
- 项目整体覆盖率不得因本功能下降。 |
||||||
|
- 对 GIL、引用计数、异常和析构代码,不能只依赖行覆盖率,必须人工检查分支矩阵。 |
||||||
|
- 最终 coding 计划必须先列出测试清单,再列实现任务。 |
||||||
|
|
||||||
|
## 22. 已确认与待确认问题 |
||||||
|
|
||||||
|
已确认: |
||||||
|
|
||||||
|
1. Python 互调用是可选的扩展级特性;TypePHP 不链接或在编译期检查 `libphpy.so`,首次实际调用时若 phpy 未加载则由 Zend 抛出 PHP `Error`。 |
||||||
|
2. TypePHP 尽可能采用显式转换,不继承 phpy 的全部隐式转换行为。 |
||||||
|
3. TypePHP 运算符在编译期改写为 `operator\add($left, $right)` 一类 Python 标准库调用,不使用 phpy opcode handler,也不生成 phpy C++ 符号调用。 |
||||||
|
4. `python` 根命名空间大小写不敏感,其后的所有 Python 符号大小写敏感。 |
||||||
|
5. `python` 是编译器处理的特殊语言命名空间。 |
||||||
|
6. 运行时类继续使用 `PyObject`、`PyDict` 等 phpy 公开名称。 |
||||||
|
7. `python\dict()` 等构造语法是现有 phpy 类构造器的语法糖;`python\print()` 等是 `PyCore` API 的语法糖。 |
||||||
|
8. `new PyList()` 与 `python\list()` 具有相同的 `PyList` typed object 类型和优化能力。 |
||||||
|
9. phpy 解决运行时问题,TypePHP 只通过缓存的 `zend_function*` 和 PHPX/Zend 通用对象 API 动态调用 phpy Facade。 |
||||||
|
10. TypePHP 的 Python 专用实现与测试放入独立子目录,通过受控入口接入通用编译流程。 |
||||||
|
11. Python 线程、`asyncio` 和 subinterpreter 永久禁止,且不作为后续兼容目标。 |
||||||
|
12. `===` / `!==` 分别映射 Python identity 的 `is` / `is not`;`==` / `!=` 使用 Python 值比较。 |
||||||
|
13. 仅支持 TypePHP 主动调用 Python;不生成 Python extension,不提供 `#[PythonExport]`,不向 Python 注册 TypePHP 符号。 |
||||||
|
14. CPython 和 bridge 生命周期完全复用 phpy 的 `MINIT/RINIT/RSHUTDOWN/MSHUTDOWN` 入口。 |
||||||
|
15. Python 包变量使用 PHP namespace constant 语法 `math\pi` 读取,但在运行时执行动态 Python attribute lookup;`math::pi` 和 `math::$pi` 是错误的 class member 表达法。 |
||||||
|
16. `np\array()` 表示读取并调用 Python 包成员;该成员可以是函数、class 或其他 callable,具体类型由 Python 运行时决定。 |
||||||
|
17. `PyObject` 可以与 TypePHP 值混合运算;TypePHP 操作数转换为 Python 对象后,整个运算由 CPython protocol 执行,结果保持为 `PyObject`。 |
||||||
|
18. Python 函数、方法、class 构造和 builtin 调用的结果一律保持为 `PyObject` 或已知的 phpy 子类;禁用 phpy 返回值隐式转换。 |
||||||
|
19. `PyObject::toValue()` 是显式标量/容器转换方法,等价于 `python\scalar()`;`PyObject::toArray()` 只接受可转换容器和 iterator,不支持的类型返回空数组。转换后可继续使用普通 TypePHP 转换,例如 `$obj->toValue()->toInt()`。 |
||||||
|
20. TypePHP 调用 Python 时,所有参数自动转换为 Python 类型;TypePHP 数组递归深拷贝,空数组默认转换为 Python list。 |
||||||
|
21. 性能敏感代码应复用 `PyDict`、`PyList`、`PyStr` 等代理对象,避免同一 TypePHP 值反复转换和深拷贝。 |
||||||
|
22. TypePHP 的主要语言增量是 `use python\...` 和模块别名;使用别名时通过与 `funcMap` 同类的 lazy indexed map 调用 phpy import,其他运行时能力优先直接复用 phpy。 |
||||||
|
23. `use python\module` 完全交由 PHP namespace 解析处理;当前 `.php` 文件没有实际访问解析到该 module 的符号时,不生成 helper,也不执行运行时 import。 |
||||||
|
24. 发现 `module\attr` 或 `module\func()` 时,才为完整 module 名称分配 ID;未使用的 `use` 不占 map slot,也不执行 import。 |
||||||
|
25. `pythonModuleMap` 与 `funcMap` 一样集中声明、按 ID lazy lookup;区别是 module 保存为拥有引用的 Zend object zval,必须在 request clean 中逐项 `zval_ptr_dtor()` 并恢复为 `UNDEF`。 |
||||||
|
26. `sys.modules` 负责全局加载状态和 identity,`pythonModuleMap` 只表示 TypePHP 已经完成并缓存的 module binding。 |
||||||
|
27. TypePHP 生成代码只依赖 PHPX/ZendVM;`PyCore::import()`、builtin、对象方法和转换均解析为 `zend_function*` 动态调用。 |
||||||
|
28. Python 运算符隐式使用 `python\operator` module;完整运算协议由 CPython `operator` 函数处理,不直接调用 dunder,也不由 TypePHP 实现 reflected fallback。 |
||||||
|
|
||||||
|
模块 namespace attribute 初版只读。PHP namespace constant 表达式不能作为赋值目标;后续若增加写入能力,应采用显式 API,并在确定语义后先补测试。 |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
# TypePHP Python 互调用分阶段实施计划 |
||||||
|
|
||||||
|
> 本计划以 `python/design.md` 为规范。每个阶段严格执行:先增加 PHPUnit/PHPT/pytest 测试并确认失败,再实现,再运行相关测试和完整回归。 |
||||||
|
|
||||||
|
## 阶段 1:Python module name、use 与 lazy binding |
||||||
|
|
||||||
|
目标是完成最小可运行闭环,不实现运算符和通用转换: |
||||||
|
|
||||||
|
1. 识别全局 namespace 中的 `python\module\member()` / `python\module\member`、其他 namespace 中的 `\python\module\member()` / `\python\module\member`、可选的 `use python\module` 简写、根名称大小写不敏感和 Python 后续名称大小写敏感。 |
||||||
|
2. 不建立 Python 专用 alias 表,也不特殊处理 `use`;使用 PHP 的普通 namespace、`use function`、`use const`、`as` alias、冲突检查和完整名称解析。在 namespace 内必须写成 `\python\module`,相对的 `python\module` 仍解析为当前 PHP namespace 下的名称。 |
||||||
|
3. 仅在出现 `python\module\attr`、`python\module\func()` 或其 alias 形式时分配 module ID。 |
||||||
|
4. 生成与 `funcMap` 同类的 `pythonModuleMap`、lazy getter 和 request-clean 代码。 |
||||||
|
5. 使用 Zend class/function map 动态调用 `PyCore::import()`;不 include、link 或检测 phpy。 |
||||||
|
6. 使用 Zend object API 读取 module 属性及调用 module callable。 |
||||||
|
7. phpy 未加载时在首次实际使用处抛出 PHP `Error`;未使用的 Python use 不触发错误。 |
||||||
|
|
||||||
|
测试顺序:PHPUnit 代码生成与诊断测试 → PHPT 运行时测试 → 现有 compiler 回归。 |
||||||
|
|
||||||
|
实现状态:已完成。完整名称与任意 alias 按 Python dotted module name 共享同一个 runtime slot;完整名称不要求 `use`,两种语法都在首次实际执行时 lazy import。识别前严格采用 PHP namespace resolution:例如在 `namespace App` 中,相对名称 `python\math\sqrt()` 是普通的 `App\python\math\sqrt()`,只有 `\python\math\sqrt()` 指向 Python 根命名空间。 |
||||||
|
|
||||||
|
## 阶段 2:builtins、构造语法糖与静态类型 |
||||||
|
|
||||||
|
1. `python\name()` 通过 phpy Zend Facade 动态调用:显式 `PyCore` 方法直接复用,其他名称经 Python `builtins` module lookup。 |
||||||
|
2. `python\list/dict/tuple/set/str/object()` 映射既有 phpy Zend 类或方法。 |
||||||
|
3. `new PyList()` 与 `python\list()` 等写法获得相同的逻辑静态类型。 |
||||||
|
4. Python 调用结果保持 `PyObject` 或已知 phpy 子类,关闭 TypePHP 路径的隐式 scalar conversion。 |
||||||
|
5. 缺少 phpy、builtin 不存在、参数错误和异常映射测试。 |
||||||
|
|
||||||
|
实现状态:已完成。TypePHP 在首次实际执行 Python 表达式时延迟启用 phpy 的 `return_as_object`,仅声明未使用的 Python 符号仍不触发运行时依赖。 |
||||||
|
|
||||||
|
## 阶段 3:参数转换与显式结果转换 |
||||||
|
|
||||||
|
1. TypePHP 参数从左到右求值后自动转换为 Python 值。 |
||||||
|
2. 标量、数组、空数组、嵌套容器及 TypePHP callable 转换。 |
||||||
|
3. 通过 `$py->toValue()` 或 `python\scalar($py)` 离开 Python 对象规则;需要确定原生类型时继续使用普通 TypePHP 转换,例如 `$py->toValue()->toInt()`。容器和 iterator 可直接使用 `$py->toArray()`。 |
||||||
|
4. 深拷贝、递归容器、溢出、Unicode/bytes 和异常路径测试。 |
||||||
|
5. review 并重构 phpy 转换策略,移除影响同步重入的全局临时转换状态。 |
||||||
|
|
||||||
|
实现状态:核心边界已完成。TypePHP 参数严格从左到右求值,支持标量、空数组、嵌套 list/dict 与 callable;`PyObject::toValue()` 与 `python\scalar()` 复用 phpy 的显式转换入口,`PyObject::toArray()` 转换受支持的容器和 iterator。phpy 已移除进程级转换函数指针,改为局部有状态转换器、RAII 递归保护和 128 层深度限制,并覆盖无效 UTF-8、PHP 自引用数组及 Python 循环容器错误路径。Python 大整数与 bytes 的最终语言映射仍保留在本阶段后续工作中。 |
||||||
|
|
||||||
|
## 阶段 4:运算符 |
||||||
|
|
||||||
|
1. 将运算符改写为 Python 标准库 `operator` module 的动态调用。 |
||||||
|
2. 混合操作数先转换为 `PyObject`。 |
||||||
|
3. 严格保证从左到右、各求值一次。 |
||||||
|
4. 使用 `operator.is_/is_not/truth` 实现 identity 和 truthiness,使用 `iadd/isub/...` 实现 compound assignment。 |
||||||
|
5. 验证 `operator` 自动处理 `NotImplemented`、reflected dunder 和子类优先级。 |
||||||
|
6. 对照 phpy opcode-handler 行为,修复 `/` 错误映射 floor division 等既有问题。 |
||||||
|
|
||||||
|
实现状态:已完成。二元算术、位运算、比较、`===`/`!==`、一元运算、条件真假值、短路逻辑和复合赋值均通过隐式 `operator` module binding 执行;`/` 使用 `truediv`。混合 TypePHP 操作数由 phpy 在调用边界转换,结果继续保持 `PyObject`,比较和真假值结果显式收敛为 TypePHP `bool`。属性和下标左值由阶段 5 的动态写入协议完成回写。 |
||||||
|
|
||||||
|
## 阶段 5:完整对象协议 |
||||||
|
|
||||||
|
1. Python 对象属性读写和删除。 |
||||||
|
2. 下标读写、删除、`isset()`。 |
||||||
|
3. iterator/foreach。 |
||||||
|
4. Python callable 和 TypePHP callable proxy 的同步重入。 |
||||||
|
5. keyword argument、argument unpacking 和错误语义。 |
||||||
|
|
||||||
|
实现状态:已完成。Python proxy 的动态属性、未知方法、下标、删除、`isset()`、`foreach` 和 callable 均复用 phpy 的 Zend object protocol,不生成 phpy C++ 符号。方法、属性、下标和 callable 结果会继续传播为 `PyObject`,支持链式访问和后续 Python 运算。named argument 与 unpacking 复用统一调用参数管线并保持从左到右求值;属性和下标复合赋值使用 `operator.i*()` 的返回对象回写原左值。 |
||||||
|
|
||||||
|
phpy 同步完成了对象协议加固:`__set()` 转换引用释放、`__unset()`、list/tuple 负索引、list 删除、缺失键与 Python `None` 的 `isset()` 语义、删除和 contains 状态检查,以及 iterator/count 异常传播。相关 BUG 均由 phpy PHPUnit 与 TypePHP PHPT 独立覆盖。 |
||||||
|
|
||||||
|
## 阶段 6:phpy 稳定性与性能收尾 |
||||||
|
|
||||||
|
1. CPython/ZendVM 生命周期、GIL、owned/borrowed/stolen reference 全量审计。 |
||||||
|
2. Python/Zend 异常状态和 traceback 审计。 |
||||||
|
3. 跨 VM 引用环、析构和异常注入测试。 |
||||||
|
4. ASan/UBSan、PHP leak report、Python debug build 和压力测试。 |
||||||
|
5. 基准测试动态 Zend call、module map、参数转换和 `operator` module 调用;只优化被数据证明的热点。 |
||||||
|
6. 完整 PHPUnit、pytest、PHPT 和现有 TypePHP compiler 回归。 |
||||||
|
|
||||||
|
实现状态:已完成。第一轮 CPython 失败路径审计已覆盖通用对象、list、dict、tuple、set 的构造和下标写入,以及 sequence/set 的 `contains()`。PHP 到 Python 的 key/value 转换失败现在会立即映射为 `PyError`,所有已取得的新引用均由作用域守卫释放;构造失败不再留下未处理的 CPython error indicator,`contains()` 的 `-1` 错误结果也不再被误判为 `true`。无效 UTF-8、unhashable set member、失败后容器仍可继续使用等路径已有 phpy PHPUnit 回归测试。 |
||||||
|
|
||||||
|
第二轮审计覆盖 module import、异常转换、callable 检查和显式 iterator API。`PyImport_ImportModule()`、`PyErr_Fetch()` 和 `PyIter_Next()` 转移给调用方的新引用现在都会在 Zend wrapper 取得独立引用后统一释放;重复 import、Python 异常或显式 iterator next 不再持续增加引用计数。调用非 callable 的 Python 属性或 `PyObject` 会稳定抛出 `PyError(TypeError)`,不再因为 `PyCallable_Check()` 未设置 error indicator 而静默返回 `null`。`PyCore::next()` 也会区分正常迭代结束和 iterator 异常。上述路径均先建立失败的 phpy PHPUnit 回归测试,其中对象调用行为另有 TypePHP PHPT 集成覆盖。 |
||||||
|
|
||||||
|
第三轮审计覆盖 `PyCore` Facade 的转换失败与函数缓存。`PyCore::eval()` 会在 globals 转换失败后立即抛出 `PyError`,`PyCore::bytes()` 对非字符串标量使用转换后的 `zend_string`,两者不再解引用空指针或错误的 zval union 字段而导致进程崩溃。`PyCore::next()` 同时释放参数转换产生的 iterator 引用。builtin/operator 函数缓存改用 `std::string` 内容键,不再把请求级 `char*` 地址作为长期 key,也避免同名动态调用不断重复缓存并增加 Python function 引用计数;调用存在但不可调用的 builtin 会释放临时引用并抛出 `PyError(TypeError)`。所有问题均由先失败的独立 PHPUnit 覆盖,其中两个崩溃用禁用 core dump 的隔离进程确认退出码 139 后再修复。 |
||||||
|
|
||||||
|
第四轮审计覆盖 Python 到 PHP 的同步回调边界。phpy 会将 Python keyword arguments 转换为 Zend named parameters,并在任一位置参数或命名参数转换失败后立即停止,不会执行只接收到部分参数的 PHP callable。PHPX 为 AOT 原生闭包生成并管理 Zend `arg_info` 参数名元数据,因此 Python kwargs 可以按名称绑定到 TypePHP 闭包,而不是依赖参数位置或降级为字符串 callable。phpy PHPUnit、PHPX 单元测试和 TypePHP PHPT 分别覆盖了转换失败、Zend 命名绑定以及完整的 Python→TypePHP 回调链路。 |
||||||
|
|
||||||
|
第五轮审计覆盖 Python 字符串跨 Zend 边界时的异常和所有权。Python 孤立代理字符无法编码为 UTF-8 时,`phpy.String`、动态 PHP 类名、字典键、`PyObject::__toString()` 和 Python 异常消息格式化都不会再使用空指针或未初始化长度;修复前相关隔离测试会退出 139 或尝试分配异常大的内存。`StrObject` 现在具有显式有效状态,所有调用方必须在访问指针前检查转换结果;异常消息的字符串化仅作为 best-effort 辅助信息,失败时保留原始 Python error/type/value,并清理临时 CPython error indicator。`new_string()` 同时补齐 Zend carrier 析构注册,定长字符串直接取得唯一的 `zend_string` 引用,消除了成功路径的泄漏和未初始化 zval。 |
||||||
|
|
||||||
|
第六轮引用审计修复了 `PySequence::slice()` 的 new-reference 泄漏。切片在包装为 Zend `PyObject` 后会释放 CPython API 返回的原始所有权,同时保留 wrapper 自己持有的引用;由 `sys.getrefcount()` 压力测试验证重复创建并销毁切片不会继续增加元素引用计数。切片创建失败也会在接触空指针前转换为 `PyError`。 |
||||||
|
|
||||||
|
第七轮审计覆盖动态 PHP 的 Python 运算符协议。phpy opcode handler 现在使用 CPython `PyNumber_*` / `PyObject_RichCompareBool()`,`/` 与 `/=` 使用 true division,复合赋值会用 in-place API 返回的对象更新 Zend 左值,并正确处理不可变 Python 对象、Zend 引用变量、表达式结果和失败后左值状态。`===` / `!==` 使用 Python object identity,bool cast、`!` 和条件分支使用 Python truth protocol;PHP 操作数转换、结果和异常路径的新引用统一由 RAII 守卫管理。TypePHP 的 `operators.phpt` 同时以 ZendPHP + opcode handler 和 AOT + `operator` module 运行,因此是两条实现的输出一致性门禁。 |
||||||
|
|
||||||
|
动态代码的一元正负运算已确定为兼容性边界:PHP 会把 `-$value` / `+$value` 编译成乘以 `-1` / `1`,opcode handler 无法区分它与源码中的显式乘法。动态 ZendVM 代码不再尝试改写 AST,而是明确保留 `$value * -1` / `$value * 1` 的行为。Python 内置数值和 NumPy 等常见对象的结果通常一致,但自定义对象的 `__neg__()` / `__pos__()` 可能与 `__mul__()` 不同。AOT TypePHP 保留原始 AST,仍分别 lowering 为 `operator.neg()` / `operator.pos()`,语义不受影响。外部用户文档 `python.md` 已将这一差异列入兼容性限制。 |
||||||
|
|
||||||
|
性能基准分别覆盖 module property、operator module call、已有 `PyObject` 参数和 PHP 标量参数转换。在当前未优化构建中,module property 约为 0.7–0.9 μs/op,operator call 约为 1.8–2.5 μs/op;参数是否预先包装为 `PyObject` 没有呈现稳定差异。尝试以 indirect zval wrapper 消除 module map 每次访问的引用计数后,A/B 中位数仍处于同一噪声区间,因此没有保留生命周期更敏感且收益未经证明的优化。 |
||||||
|
|
||||||
|
phpy 的 CMake Python-extension 目标同时完成了 out-of-tree 构建修复,并通过从 Zend 标准 cast handler 推导返回类型来兼容旧版 PHP 的 `int` ABI 与新版 PHP 的 `zend_result` ABI。PHP 8.1 和 PHP 8.4 均已完成全新构建验证。 |
||||||
|
|
||||||
|
内存门禁使用 Valgrind Memcheck 执行。测试关闭 Zend allocator 与 PCRE JIT,在最小独立进程中分别循环 100 次 PHP Closure kwargs 回调、可调用 PHP 对象 kwargs 回调、sequence slice 创建销毁、无效 Unicode 的对象字符串化、字典键转换和异常格式化。结果为 0 invalid-access、0 definite leak、0 indirect leak;进程退出时由 PHP/CPython 保留的 493,106 bytes 均为 still-reachable,不计为泄漏。ASan 扩展无法安全 `dlopen` 到当前启用了 `RTLD_DEEPBIND` 的非 ASan PHP,因此本轮采用不要求 PHP 同步重编译的 Valgrind 作为内存检查工具。 |
||||||
|
|
||||||
|
阶段 6 最终门禁结果:phpy PHPUnit 135 tests / 469 assertions 通过(1 个既有 warning、1 个环境相关 skip),pytest 26/26 通过,TypePHP Python PHPT 14/14 通过,TypePHP PHPUnit 1103 tests / 2729 assertions 通过;TypePHP compiler 全量 PHPT 共 934 项,其中 932 PASS、2 SKIP、0 FAIL、0 WARN。动态 operator 压力测试在 Valgrind 下为 0 invalid access、0 definite leak、0 indirect leak。 |
||||||
|
|
||||||
|
## 阶段门禁 |
||||||
|
|
||||||
|
- 当前阶段的失败测试未先建立,不开始实现。 |
||||||
|
- 当前阶段所有测试未通过,不进入下一阶段。 |
||||||
|
- phpy 的行为变更必须先在 phpy 仓库增加 PHPUnit/pytest 测试。 |
||||||
|
- 每个已修复 BUG 必须保留独立回归测试。 |
||||||
|
- 不以修改第三方测试期望来掩盖实现差异。 |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
# Python 工具子模块 |
||||||
|
|
||||||
|
TypePHP 将 Python IDE helper 生成器和 Python 源码转换器集成到了 `tpc`。两者位于独立的 |
||||||
|
`src/PythonTools` 目录,只复用 `tpc` 命令入口,不进入正常的 PHP 预处理、C++ 生成和编译流水线。 |
||||||
|
|
||||||
|
## Python namespace IDE helper |
||||||
|
|
||||||
|
```shell |
||||||
|
./tpc --gen-python-helper math |
||||||
|
./tpc --gen-python-helper numpy.linalg |
||||||
|
./tpc --gen-python-helper numpy --output-dir .ide-helper |
||||||
|
``` |
||||||
|
|
||||||
|
命令通过 PHPy 导入指定 Python module,并使用 Python `inspect` API 采集函数、参数、类、方法和 |
||||||
|
module attribute。PHPy 扩展以及目标 Python module 必须安装在执行 `tpc` 的主机环境中。 |
||||||
|
|
||||||
|
默认生成文件位于当前目录的 `ide-helper` 中。`--output-dir` 可以替换这个输出根目录,既支持 |
||||||
|
相对当前目录的路径,也支持绝对路径: |
||||||
|
|
||||||
|
```text |
||||||
|
ide-helper/python/math.php |
||||||
|
ide-helper/python/numpy/linalg.php |
||||||
|
ide-helper/python.php |
||||||
|
ide-helper/PyObject.php |
||||||
|
``` |
||||||
|
|
||||||
|
每次生成 module helper 时,会同时扫描 Python `builtins` 并生成根命名空间文件 |
||||||
|
`python.php`,为 `python\tuple()`、`python\len()` 等内置符号提供 IDE 补全。该文件会 |
||||||
|
随当前 Python 环境重新生成。 |
||||||
|
|
||||||
|
首次生成 module helper 时,还会生成公共的 `PyObject.php`。它包含 `PyObject` 的动态访问、调用、 |
||||||
|
数组访问、迭代以及 `toArray()`、`toValue()` 等方法提示,供所有 Python module helper 共享。若该文件 |
||||||
|
已经存在,生成器会保留原文件,不进行覆盖。 |
||||||
|
|
||||||
|
生成内容使用 TypePHP 的 module-as-namespace 形式,例如 `python\math\sqrt()`,并兼容普通 |
||||||
|
`use`、`use function` 和 `use const` 的 IDE 名称解析。文件末尾包含 `die`,用于在误执行时明确 |
||||||
|
终止程序。helper 只能交给 IDE 索引,不能被 include,也不能加入 TypePHP 项目的 sources 或编译输入。 |
||||||
|
|
||||||
|
`PyObject::IDE_HELPER_ONLY` 是所有 helper 共用的提示常量。非 `void` stub 的方法体使用 |
||||||
|
`die(\PyObject::IDE_HELPER_ONLY)`,以满足 IDE 对返回类型控制流的检查,不会再产生“缺少 return |
||||||
|
语句”的诊断。module attribute 使用命名空间 `const` 声明,支持 IDE 的常量补全和 `use const`。 |
||||||
|
PHP 8.1 及以上允许在常量初始化表达式中使用 `new`。module attribute 因此直接使用仅供 IDE |
||||||
|
分析的 `PyObject` 实例作为占位值: |
||||||
|
|
||||||
|
```php |
||||||
|
const pi = new \PyObject(); |
||||||
|
``` |
||||||
|
|
||||||
|
这样 IDE 会将常量精确识别为 `PyObject`,而不是从 `null` 推断出错误类型。 |
||||||
|
|
||||||
|
公共 `PyObject` helper 还声明了 TypePHP 的虚拟关键词方法,包括 `toInt()`、`toFloat()`、 |
||||||
|
`toString()`、`toBool()`、`toStream()`、高精度类型转换、`toObject()`、`toAny()` 和 `toRef()`。 |
||||||
|
这些声明仅用于 IDE 补全;调用会在编译期展开,并不是 PHPy `PyObject` 运行时类的实体方法。 |
||||||
|
`toArray()` 和 `toValue()` 则仍是 PHPy 提供的真实方法。 |
||||||
|
|
||||||
|
Python class 的构造函数会显式调用 `parent::__construct()`。Python 对象若定义了 `count()`,helper |
||||||
|
不会重复声明它,因为 `PyObject::count(): int` 已用于 PHP `Countable`。需要调用 Python 自身的 |
||||||
|
`count()` 时,应显式写为 `$object->__call('count', $arguments)`。 |
||||||
|
|
||||||
|
PHP function/class 名称大小写不敏感,而 Python 名称大小写敏感;PHP 保留字也不能声明为普通 |
||||||
|
stub symbol。生成器会以注释报告无法用合法 PHP 声明表达的符号,不会擅自重命名 Python API。 |
||||||
|
`python\print()` 的调用语法合法,但 PHP 禁止声明名为 `print` 的函数,因此单纯的 |
||||||
|
PHP helper 文件无法为它提供无语法错误的符号声明。`list`、`int`、`float` 等 PHP |
||||||
|
保留字存在同样的限制。 |
||||||
|
|
||||||
|
## Python 转 TypePHP |
||||||
|
|
||||||
|
```shell |
||||||
|
./tpc --convert-python-to-php script.py > script.php |
||||||
|
``` |
||||||
|
|
||||||
|
转换器调用 PATH 中的 `python3` 解析 Python AST,然后输出使用 TypePHP Python namespace |
||||||
|
语法的 PHP 源码。普通 module import 会转换为 namespace import: |
||||||
|
|
||||||
|
```python |
||||||
|
import math |
||||||
|
print(math.sqrt(16)) |
||||||
|
``` |
||||||
|
|
||||||
|
```php |
||||||
|
use python\math; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
python\print(math\sqrt(16)); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
当前支持普通 import、函数、赋值、调用、容器字面量、基础运算、单项比较、if/while/for、 |
||||||
|
lambda 和基础 f-string。module 顶层变量会转换为 PHP global,以保持函数读取 module 变量的能力。 |
||||||
|
当语义可以严格保持时,转换器会直接使用 PHP 原生语法:无参数或可安全转换的 |
||||||
|
`print()` 生成带换行的 `echo`,`sys.exit()` 和整数字面量退出码生成 `exit`。具有 |
||||||
|
`sep`、`end`、`file`、`flush` 参数的 `print()`,以及字符串或对象形式的 `sys.exit()` |
||||||
|
与 PHP 行为不完全一致,仍保留为 Python 调用。 |
||||||
|
|
||||||
|
转换器遵循“不能可靠保持语义就拒绝”的原则。class、async、generator、try/with、decorator、 |
||||||
|
destructuring assignment、chained comparison、嵌套函数以及 loop-else 等尚未完成的语法会抛出带 |
||||||
|
源文件和行号的错误,不会生成看似可用但语义错误的 PHP 代码。 |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
## 编译 |
||||||
|
|
||||||
|
```shell |
||||||
|
./tpc --wasm test.php |
||||||
|
``` |
||||||
|
|
||||||
|
编译成功后默认只生成可由 Wasmtime 执行的 WASI 0.2 Component `test.wasm`。WASI 0.1 不受支持。 |
||||||
|
生成的 C++ 源码默认写入 `build/`,也可以通过 `--build-dir <directory>` 指定。 |
||||||
|
|
||||||
|
## 执行 |
||||||
|
|
||||||
|
```shell |
||||||
|
wasmtime test.wasm |
||||||
|
``` |
||||||
|
|
||||||
|
## Chrome |
||||||
|
|
||||||
|
```shell |
||||||
|
./tpc --wasm=browser test.php |
||||||
|
``` |
||||||
|
|
||||||
|
浏览器模式额外生成 `test.browser/` Jco 模块并要求 `jco` 位于 `PATH`。完整浏览器 Demo 位于仓库 `examples/wasm-hello/`,并使用 `wasm: browser` 的 `project.yml` 构建。TypePHP 在专用 Worker 中执行;默认文件系统驻留内存,可显式启用 OPFS 快照持久化。网络 socket、进程、shell 和信号在 WASI 目标下明确不支持。 |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PARAMETER)] |
||||||
|
class Immutable |
||||||
|
{ |
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
class UserTest { |
||||||
|
private string $name; |
||||||
|
|
||||||
|
#[Immutable] |
||||||
|
function foo(): void |
||||||
|
{ |
||||||
|
// 不允许,方法是 Immutable 的,不可修改对象的属性 |
||||||
|
$this->name = 'hello'; |
||||||
|
} |
||||||
|
|
||||||
|
function bar( |
||||||
|
#[Immutable] |
||||||
|
string $name): void |
||||||
|
{ |
||||||
|
// 允许,方法不是 Immutable 的 |
||||||
|
$this->name = 'hello'; |
||||||
|
// 不允许,$name 是 Immutable 的,不可修改 |
||||||
|
$name = 'world'; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,15 @@ |
|||||||
|
<?php |
||||||
|
declare(strict_types=1); |
||||||
|
use native_types; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$integer = std::bigInt("123456789012345678901234567890"); |
||||||
|
echo ($integer * 9)->toString(), "\n"; |
||||||
|
|
||||||
|
$float = std::bigFloat("1000000000000000000000000000000"); |
||||||
|
echo ($float + std::bigFloat("1"))->toString(), "\n"; |
||||||
|
|
||||||
|
$decimal = std::decimal("12345.00000000000000001"); |
||||||
|
echo ($decimal + std::decimal("3.14159265358979323"))->toString(), "\n"; |
||||||
|
} |
||||||
@ -0,0 +1,6 @@ |
|||||||
|
<?php |
||||||
|
function main() |
||||||
|
{ |
||||||
|
$homepage = file_get_contents('https://www.example.com/'); |
||||||
|
echo $homepage; |
||||||
|
} |
||||||
@ -0,0 +1,51 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
#[Native] |
||||||
|
#[Printer] |
||||||
|
class Point |
||||||
|
{ |
||||||
|
public int $x; |
||||||
|
public int $y; |
||||||
|
|
||||||
|
function foo() |
||||||
|
{ |
||||||
|
var_dump($this->x, $this->y); |
||||||
|
} |
||||||
|
|
||||||
|
function toBool(): bool |
||||||
|
{ |
||||||
|
return $this->x != 0 || $this->y != 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function bar(Point $point) |
||||||
|
{ |
||||||
|
$point->x += 333; |
||||||
|
$point->y += 777; |
||||||
|
} |
||||||
|
|
||||||
|
function main() |
||||||
|
{ |
||||||
|
$p = new Point(); |
||||||
|
$p->x = 100; |
||||||
|
$p->y = 900; |
||||||
|
echo $p, "\n"; |
||||||
|
$p->foo(); |
||||||
|
|
||||||
|
$array = std::array(Point::class, 10); |
||||||
|
$array[0] = $p; |
||||||
|
|
||||||
|
echo $array[0], "\n"; |
||||||
|
$array[0]->foo(); |
||||||
|
|
||||||
|
// bar($array[0]); |
||||||
|
// echo $array[0], "\n"; |
||||||
|
|
||||||
|
bar($p); |
||||||
|
echo $p, "\n"; |
||||||
|
|
||||||
|
$p2 = new Point(); |
||||||
|
if ($p2->toBool()) { |
||||||
|
echo "p2 is not null\n"; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,124 @@ |
|||||||
|
# 海贼王 · 斗地主(Win32 / TypePHP 示例) |
||||||
|
|
||||||
|
将 `HelgeSverre-libui-sdk/examples/onepiece-doudizhu.php`(libui 版)移植为 |
||||||
|
**TypePHP(tpc.exe)AOT 编译的原生 Win32 程序**,参考 `landlord-win32` 的 |
||||||
|
「纯 PHP 逻辑 + C++ 薄封装 Win32 绑定」架构。 |
||||||
|
|
||||||
|
## 特性 |
||||||
|
|
||||||
|
- 完整保留原作玩法:三大势力(海军 / 七武海 / 四皇)、9 名角色技能、 |
||||||
|
叫地主 → 出牌 → 结算全流程,内置 AI 对手与托管。 |
||||||
|
- 界面由 Win32 GDI 自绘(海洋渐变背景、势力配色卡牌、对手面板、 |
||||||
|
可拖拽手牌、底部操作按钮)。 |
||||||
|
- 游戏逻辑 100% 在 PHP 中实现(`php-src/doudizhu/`),与 libui 版共用同一套 |
||||||
|
领域模型;C++(`cpp-src/win32.cc`)仅封装窗口、消息循环与 GDI 绘制原语。 |
||||||
|
- 单文件 exe,无外部 PHP 依赖,不依赖 libui / mbstring / miniaudio。 |
||||||
|
|
||||||
|
## 目录结构 |
||||||
|
|
||||||
|
``` |
||||||
|
onepiece-doudizhu-win32/ |
||||||
|
├── main.php 入口:Win32 消息循环驱动 GameController |
||||||
|
├── project.yml tpc.exe 构建配置(mode: bin) |
||||||
|
├── build.bat 一键编译脚本(需 VS 2022 x64 工具链环境) |
||||||
|
├── cpp-src/ |
||||||
|
│ └── win32.cc Win32 窗口 + GDI 绘制原语(C++ 薄封装) |
||||||
|
└── php-src/ |
||||||
|
├── win32.stub.php win_* 原生函数声明(stub) |
||||||
|
└── doudizhu/ 游戏域模型 + 渲染 shim |
||||||
|
├── Card / Deck / Combo / MoveGenerator / Game / Ai / Skill / |
||||||
|
│ Character / Faction / PlayerState 纯逻辑(与 libui 版同源) |
||||||
|
├── GameController.php 对局编排 + Win32 输入/渲染适配 |
||||||
|
├── Render.php libui DrawContext 兼容 shim → GDI |
||||||
|
└── Sound.php 音效管理器(TypePHP 版为空实现,保留 API) |
||||||
|
``` |
||||||
|
|
||||||
|
## 编译 |
||||||
|
|
||||||
|
在 **x64 Native Tools Command Prompt for VS 2022**(或已加载 |
||||||
|
`vcvars64.bat` 的终端)中执行: |
||||||
|
|
||||||
|
```bat |
||||||
|
build.bat |
||||||
|
``` |
||||||
|
|
||||||
|
脚本会: |
||||||
|
1. 校验 `cl.exe` 可用(不在 VS 环境中会提示)。 |
||||||
|
2. 从 TypePHP 根目录调用 `tpc.exe project.yml`(tpc 打包后按 CWD 解析 |
||||||
|
`vendor/autoload.php`,必须从根目录运行)。 |
||||||
|
3. 将产物 `onepiece_doudizhu.exe` 复制回示例目录。 |
||||||
|
|
||||||
|
等价手动命令: |
||||||
|
|
||||||
|
```bat |
||||||
|
set PHP_HOME=D:\git\php\tpc_v1095_windows_x86_64 |
||||||
|
set PHPX_HOME=%PHP_HOME%\phpx |
||||||
|
set PATH=%PHP_HOME%;%PATH% |
||||||
|
cd /d D:\git\php\tpc_v1095_windows_x86_64 |
||||||
|
tpc.exe examples\onepiece-doudizhu-win32\project.yml --no-progress |
||||||
|
``` |
||||||
|
|
||||||
|
## 运行 |
||||||
|
|
||||||
|
```bat |
||||||
|
onepiece_doudizhu.exe |
||||||
|
``` |
||||||
|
|
||||||
|
- 拖拽手牌选牌,底部按钮:出牌 / 不出 / 提示 / 技能 / 托管。 |
||||||
|
- 右上角按钮切换音效(当前为占位实现)。 |
||||||
|
- 关闭窗口或 `Esc` 退出。 |
||||||
|
|
||||||
|
## 移植要点 |
||||||
|
|
||||||
|
1. **渲染 shim(Render.php)**:原 `GameController` 的绘制代码直接使用 |
||||||
|
libui 的 `DrawContext / Brush / Color / FontDescriptor / DrawTextAlign / |
||||||
|
TextWeight`,这些符号在 `php-src/doudizhu/Render.php` 中同命名空间重新声明, |
||||||
|
`WinDrawContext` 把每个调用翻译为 `win_fill_rect / win_fill_ellipse / |
||||||
|
win_fill_rounded_rect / win_stroke_rounded_rect / win_draw_text_ex`。 |
||||||
|
渐变近似为纯色填充;颜色统一 `0xRRGGBB → COLORREF(0xBBGGRR)` 转换。 |
||||||
|
2. **输入驱动**:libui 的 `AreaDelegate::mouse/key` 改为 `main.php` 主循环 |
||||||
|
轮询 `win_peek_message()`,把 `WM_LBUTTONDOWN/UP/MOUSEMOVE/KEYDOWN` |
||||||
|
转换为 `GameController::onMouse()/onKey()` 调用。 |
||||||
|
3. **定时器**:`Loop::delay` 改为 `GameController::tick()`,由主循环每帧 |
||||||
|
调用,驱动 AI 走子 / 叫分 / 托管(基于 `win_get_tick_count()`)。 |
||||||
|
4. **TypePHP 兼容性修正**(相对 libui 版源码): |
||||||
|
- 顶层游离代码(`define('FONT', …)`)包装进 `ensureDdzFont()`,由 |
||||||
|
`main()` 调用——TPC 要求所有执行代码位于函数内。 |
||||||
|
- `readonly` 属性 / `&$ref` 引用遍历等改为普通属性 / 下标赋值(TPC 语法限制)。 |
||||||
|
- `mb_strlen / mb_substr` 改为 `ddz_utf8_len / ddz_utf8_substr` |
||||||
|
(PCRE 实现,TPC 运行时未链接 mbstring)。 |
||||||
|
- `Brush::linearGradient()` 签名改为与 libui 一致的 |
||||||
|
`(x0,y0,x1,y1, stops)` 形式,避免 TPC 变参类型推断失败。 |
||||||
|
5. **双缓冲位图管理(win32.cc)**:修复 `GetStockObject(BITMAP)` 用法错误 |
||||||
|
(`BITMAP` 是类型不是常量),改为保存/恢复 DC 原 bitmap 后再删除离屏位图。 |
||||||
|
6. **窗口尺寸(win32.cc)**:`win_create_window` 把传入宽高当作**客户区**尺寸 |
||||||
|
(`AdjustWindowRect` 自动补上标题栏/边框),保证底部按钮完整可见;窗口 |
||||||
|
样式为 `WS_OVERLAPPEDWINDOW`,支持拖拽缩放与最大化。新增 |
||||||
|
`win_get_client_size()` 返回当前客户区尺寸,`render()`/`drawButtons()` |
||||||
|
每帧按实际尺寸自适应布局(绘制与点击命中均基于当前尺寸记录的矩形)。 |
||||||
|
7. **运行时兼容性**:`MoveGenerator::all()` 的 `$cnt` 闭包改为 |
||||||
|
`count($byRank[$r] ?? [])`——原实现 `count($byRank[$r])` 在顺子/连对 |
||||||
|
枚举缺失 rank 时对 null 调用 `count()`,TypePHP 运行时(PHP 8 严格类型) |
||||||
|
抛 `TypeError: count(): Argument #1 must be of type Countable|array`。 |
||||||
|
8. **窗口标题乱码(win32.cc)**:本机腾讯电脑管家(`tsbx.dll`)会 inline-hook |
||||||
|
进程内 user32 的 `DefWindowProcA` / `CreateWindowExW` / `SetWindowTextW`, |
||||||
|
导致窗口标题被改写为乱码("wm<崑s " 之类)。修复: |
||||||
|
- `DdzWndProc` 末尾显式调用 **`DefWindowProcW`**(而非 `DefWindowProc` 宏, |
||||||
|
后者在非 UNICODE 编译下展开为被 hook 的 `DefWindowProcA`)。 |
||||||
|
- `php_win_create_window` 通过 `LoadLibraryW("C:\\Windows\\System32\\user32.dll")` |
||||||
|
+ `GetProcAddress` 解析**原始** `CreateWindowExW` / `SetWindowTextW` 调用, |
||||||
|
创建后再用原始 `SetWindowTextW` 重设一次标题(双保险)。 |
||||||
|
- 对照组验证:Explorer / Edge / SmartGit 等其它进程窗口标题读取正常, |
||||||
|
仅本进程自定义 WndProc 窗口受影响,证明是进程内 hook 而非系统/代码问题。 |
||||||
|
9. **托管节奏(GameController.php)**:Win32 移植版里 `defer()` 与 `delay()` |
||||||
|
都由主循环 `tick()` 驱动、机制相同。原代码在托管/AI 走子/叫分处同时挂 |
||||||
|
`defer`(立即执行)+ `delay`(兜底),导致动作在下一帧瞬间完成,玩家 |
||||||
|
来不及点「取消托管」。修复: |
||||||
|
- 去掉托管分支的 `defer`,只保留 **1500ms** 延迟(充足取消窗口)。 |
||||||
|
- `scheduleAi()` 去掉 `defer`,AI 出牌节奏改为 **800ms**。 |
||||||
|
- `scheduleBidStep()` 去掉 `defer`,AI 叫分节奏改为 **700ms**。 |
||||||
|
- `toggleAutoPlay()` 关闭托管时调用 `cancelTimers()` 立即取消挂起的 |
||||||
|
托管定时器;`autoPlayStep()` 原有 `!$this->autoPlay` 保护兜底。 |
||||||
|
- `toggleAutoPlay()` 关闭托管且轮到玩家时,重新调用 |
||||||
|
`setActionsForHumanTurn()` 重算按钮状态——托管时「出牌/不出/提示/技能」 |
||||||
|
按 `!autoPlay` 被禁用,恢复后必须重算才能重新可点。 |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
@echo off |
||||||
|
rem ============================================================ |
||||||
|
rem Build script for the One Piece Dou Dizhu Win32 example. |
||||||
|
rem |
||||||
|
rem Run this from the "x64 Native Tools Command Prompt for VS 2022" |
||||||
|
rem (the MSVC environment must be active so that cl.exe, INCLUDE |
||||||
|
rem and LIB are all set up). Usage: |
||||||
|
rem |
||||||
|
rem build.bat |
||||||
|
rem |
||||||
|
rem NOTE: tpc.exe is a packaged binary and resolves vendor/autoload.php |
||||||
|
rem relative to the CURRENT WORKING DIRECTORY, so we launch it from |
||||||
|
rem the TypePHP root (TPC_ROOT), not from this examples dir. The |
||||||
|
rem finished binary is copied back into this directory. |
||||||
|
rem ============================================================ |
||||||
|
setlocal |
||||||
|
|
||||||
|
rem --- sanity check: are we inside an MSVC environment? --- |
||||||
|
where cl.exe >nul 2>&1 |
||||||
|
if errorlevel 1 ( |
||||||
|
echo [ERROR] cl.exe not found on PATH. |
||||||
|
echo Open the "x64 Native Tools Command Prompt for VS 2022" |
||||||
|
echo and run this script from there. |
||||||
|
exit /b 1 |
||||||
|
) |
||||||
|
|
||||||
|
set "TPC_ROOT=D:\git\php\tpc_v1095_windows_x86_64" |
||||||
|
set "PHP_HOME=%TPC_ROOT%" |
||||||
|
set "PHPX_HOME=%TPC_ROOT%\phpx" |
||||||
|
set "PATH=%TPC_ROOT%;%PATH%" |
||||||
|
|
||||||
|
rem --- resolve this script's directory --- |
||||||
|
set "SCRIPT_DIR=%~dp0" |
||||||
|
|
||||||
|
pushd "%TPC_ROOT%" |
||||||
|
echo [1/2] Running TypePHP compiler (tpc.exe) from %TPC_ROOT%... |
||||||
|
"%TPC_ROOT%\tpc.exe" "%SCRIPT_DIR%project.yml" --no-progress |
||||||
|
set "RESULT=%errorlevel%" |
||||||
|
popd |
||||||
|
|
||||||
|
if not "%RESULT%"=="0" ( |
||||||
|
echo. |
||||||
|
echo BUILD FAILED: tpc.exe exited with errorlevel %RESULT% |
||||||
|
exit /b 1 |
||||||
|
) |
||||||
|
|
||||||
|
rem --- copy the binary back into the example directory --- |
||||||
|
copy /Y "%TPC_ROOT%\onepiece_doudizhu.exe" "%SCRIPT_DIR%onepiece_doudizhu.exe" >nul |
||||||
|
|
||||||
|
echo. |
||||||
|
echo [2/2] Build finished. Output binary: %SCRIPT_DIR%onepiece_doudizhu.exe |
||||||
|
endlocal |
||||||
@ -0,0 +1,402 @@ |
|||||||
|
/**
|
||||||
|
* One Piece Dou Dizhu - Win32 API Layer (TypePHP example) |
||||||
|
* |
||||||
|
* C++ only wraps Win32 windowing + GDI drawing primitives. |
||||||
|
* ALL game logic and ALL rendering decisions live in PHP |
||||||
|
* (see php-src/doudizhu/GameController.php via the WinDrawContext shim). |
||||||
|
*/ |
||||||
|
|
||||||
|
#include <phpx.h> |
||||||
|
#include <windows.h> |
||||||
|
#include <cstdio> |
||||||
|
#include <cstring> |
||||||
|
#include <cwchar> |
||||||
|
#include <map> |
||||||
|
|
||||||
|
using namespace php; |
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Window & Message
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static bool g_quitRequested = false; |
||||||
|
|
||||||
|
// Per memory-DC frame state: the off-screen bitmap plus the DC's previous
|
||||||
|
// bitmap (so we can restore it before deleting the off-screen bitmap).
|
||||||
|
struct FrameState { |
||||||
|
HBITMAP bitmap; |
||||||
|
HBITMAP oldBitmap; |
||||||
|
}; |
||||||
|
static std::map<HDC, FrameState> g_frames; |
||||||
|
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
// Anti-hook: some Chinese security suites (e.g. Tencent PC Manager's
|
||||||
|
// tsbx.dll) inline-hook the user32 exports (CreateWindowExW /
|
||||||
|
// SetWindowTextW) in every process, rewriting window titles to garbage.
|
||||||
|
// To get a reliable title we resolve the ORIGINAL exports from a freshly
|
||||||
|
// loaded copy of user32.dll and call those instead of the hooked IAT
|
||||||
|
// entries. All other Win32 calls are unaffected and stay normal.
|
||||||
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
|
typedef HWND (WINAPI *RealCreateWindowExW)(DWORD, LPCWSTR, LPCWSTR, DWORD, int, int, int, int, HWND, HMENU, HINSTANCE, LPVOID); |
||||||
|
typedef BOOL (WINAPI *RealSetWindowTextW)(HWND, LPCWSTR); |
||||||
|
typedef int (WINAPI *RealGetWindowTextW)(HWND, LPWSTR, int); |
||||||
|
|
||||||
|
static HMODULE g_realUser32 = NULL; |
||||||
|
static RealCreateWindowExW g_realCreateWindowExW = NULL; |
||||||
|
static RealSetWindowTextW g_realSetWindowTextW = NULL; |
||||||
|
static RealGetWindowTextW g_realGetWindowTextW = NULL; |
||||||
|
|
||||||
|
static void resolve_real_user32(void) { |
||||||
|
if (g_realCreateWindowExW != NULL) { |
||||||
|
return; |
||||||
|
} |
||||||
|
g_realUser32 = LoadLibraryW(L"C:\\Windows\\System32\\user32.dll"); |
||||||
|
if (g_realUser32 == NULL) { |
||||||
|
g_realUser32 = GetModuleHandleW(L"user32.dll"); |
||||||
|
} |
||||||
|
if (g_realUser32 != NULL) { |
||||||
|
g_realCreateWindowExW = (RealCreateWindowExW)GetProcAddress(g_realUser32, "CreateWindowExW"); |
||||||
|
g_realSetWindowTextW = (RealSetWindowTextW)GetProcAddress(g_realUser32, "SetWindowTextW"); |
||||||
|
g_realGetWindowTextW = (RealGetWindowTextW)GetProcAddress(g_realUser32, "GetWindowTextW"); |
||||||
|
} |
||||||
|
// Fallbacks to the (possibly hooked) IAT entries if resolution failed.
|
||||||
|
if (g_realCreateWindowExW == NULL) g_realCreateWindowExW = CreateWindowExW; |
||||||
|
if (g_realSetWindowTextW == NULL) g_realSetWindowTextW = SetWindowTextW; |
||||||
|
if (g_realGetWindowTextW == NULL) g_realGetWindowTextW = GetWindowTextW; |
||||||
|
} |
||||||
|
|
||||||
|
LRESULT CALLBACK DdzWndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { |
||||||
|
switch (msg) { |
||||||
|
case WM_CLOSE: |
||||||
|
case WM_DESTROY: |
||||||
|
g_quitRequested = true; |
||||||
|
PostQuitMessage(0); |
||||||
|
return 0; |
||||||
|
case WM_PAINT: |
||||||
|
// We render the whole client area every frame via GetDC, so just
|
||||||
|
// validate the paint region to avoid an endless WM_PAINT loop.
|
||||||
|
ValidateRect(hWnd, NULL); |
||||||
|
return 0; |
||||||
|
} |
||||||
|
// NOTE: must call DefWindowProcW (not the DefWindowProc macro). On this
|
||||||
|
// machine Tencent PC Manager (tsbx.dll) inline-hooks DefWindowProcA and
|
||||||
|
// corrupts UTF-16 window titles (they come back as garbage). The W
|
||||||
|
// variant is not hooked.
|
||||||
|
return DefWindowProcW(hWnd, msg, wParam, lParam); |
||||||
|
} |
||||||
|
|
||||||
|
Int php_win_create_window(String title, Int width, Int height) { |
||||||
|
SetConsoleOutputCP(65001); |
||||||
|
resolve_real_user32(); |
||||||
|
|
||||||
|
WNDCLASSW wc; |
||||||
|
ZeroMemory(&wc, sizeof(wc)); |
||||||
|
wc.style = CS_HREDRAW | CS_VREDRAW; |
||||||
|
wc.lpfnWndProc = DdzWndProc; |
||||||
|
wc.hInstance = GetModuleHandle(NULL); |
||||||
|
wc.hCursor = LoadCursor(NULL, IDC_ARROW); |
||||||
|
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); |
||||||
|
wc.lpszClassName = L"DdzWindow"; |
||||||
|
RegisterClassW(&wc); |
||||||
|
|
||||||
|
// UTF-8 (PHP string) -> UTF-16 window title.
|
||||||
|
int wtitle_len = MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, NULL, 0); |
||||||
|
wchar_t* wtitle = new wchar_t[wtitle_len]; |
||||||
|
MultiByteToWideChar(CP_UTF8, 0, title.data(), -1, wtitle, wtitle_len); |
||||||
|
|
||||||
|
// The requested width/height are treated as the CLIENT area, so every
|
||||||
|
// control the PHP layout draws is fully visible. The outer window frame
|
||||||
|
// (title bar + borders) is added on top via AdjustWindowRect.
|
||||||
|
RECT rc = {0, 0, (int)width, (int)height}; |
||||||
|
DWORD style = WS_OVERLAPPEDWINDOW; // resizable + maximizable
|
||||||
|
AdjustWindowRect(&rc, style, FALSE); |
||||||
|
int winW = rc.right - rc.left; |
||||||
|
int winH = rc.bottom - rc.top; |
||||||
|
|
||||||
|
// Use the ORIGINAL (un-hooked) CreateWindowExW so security-suite hooks
|
||||||
|
// cannot corrupt the window title. Fall back to the normal API if the
|
||||||
|
// real one could not be resolved.
|
||||||
|
HWND hWnd = g_realCreateWindowExW( |
||||||
|
0, L"DdzWindow", wtitle, |
||||||
|
style, |
||||||
|
CW_USEDEFAULT, CW_USEDEFAULT, |
||||||
|
winW, winH, |
||||||
|
NULL, NULL, GetModuleHandle(NULL), NULL |
||||||
|
); |
||||||
|
|
||||||
|
// Belt & braces: re-assert the title through the original SetWindowTextW
|
||||||
|
// (some hooks corrupt the title during CreateWindowExW itself).
|
||||||
|
if (hWnd != NULL && g_realSetWindowTextW != NULL) { |
||||||
|
g_realSetWindowTextW(hWnd, wtitle); |
||||||
|
} |
||||||
|
|
||||||
|
delete[] wtitle; |
||||||
|
return (Int)hWnd; |
||||||
|
} |
||||||
|
|
||||||
|
/** Return the current client-area size as [width, height]. */ |
||||||
|
Array php_win_get_client_size(Int hWnd) { |
||||||
|
RECT rc; |
||||||
|
GetClientRect((HWND)hWnd, &rc); |
||||||
|
Array result; |
||||||
|
result.append((Int)rc.right); |
||||||
|
result.append((Int)rc.bottom); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_show_window(Int hWnd, Int cmdShow) { |
||||||
|
ShowWindow((HWND)hWnd, (int)cmdShow); |
||||||
|
} |
||||||
|
|
||||||
|
Bool php_win_quit_requested() { |
||||||
|
return g_quitRequested; |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_post_quit(Int exitCode) { |
||||||
|
PostQuitMessage((int)exitCode); |
||||||
|
} |
||||||
|
|
||||||
|
/**
|
||||||
|
* Drain one queued message and translate it into a typed array: |
||||||
|
* [type, a, b, c] |
||||||
|
* type 0 = unhandled (still dispatched, ignored by PHP) |
||||||
|
* type 1 = mouse down : a=x, b=y |
||||||
|
* type 2 = mouse up : a=x, b=y |
||||||
|
* type 3 = mouse move : a=x, b=y, c=leftHeld(0/1) |
||||||
|
* type 4 = key down : a=vk |
||||||
|
* Returns empty array when no message is pending. |
||||||
|
*/ |
||||||
|
Array php_win_peek_message() { |
||||||
|
MSG msg; |
||||||
|
ZeroMemory(&msg, sizeof(msg)); |
||||||
|
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { |
||||||
|
Array result; |
||||||
|
int type = 0; |
||||||
|
int a = 0, b = 0, c = 0; |
||||||
|
switch ((UINT)msg.message) { |
||||||
|
case WM_LBUTTONDOWN: |
||||||
|
type = 1; |
||||||
|
a = (int)(msg.lParam & 0xFFFF); |
||||||
|
b = (int)((msg.lParam >> 16) & 0xFFFF); |
||||||
|
break; |
||||||
|
case WM_LBUTTONUP: |
||||||
|
type = 2; |
||||||
|
a = (int)(msg.lParam & 0xFFFF); |
||||||
|
b = (int)((msg.lParam >> 16) & 0xFFFF); |
||||||
|
break; |
||||||
|
case WM_MOUSEMOVE: |
||||||
|
type = 3; |
||||||
|
a = (int)(msg.lParam & 0xFFFF); |
||||||
|
b = (int)((msg.lParam >> 16) & 0xFFFF); |
||||||
|
c = (msg.wParam & MK_LBUTTON) ? 1 : 0; |
||||||
|
break; |
||||||
|
case WM_KEYDOWN: |
||||||
|
type = 4; |
||||||
|
a = (int)msg.wParam; |
||||||
|
break; |
||||||
|
default: |
||||||
|
type = 0; |
||||||
|
a = (int)msg.message; |
||||||
|
b = (int)msg.wParam; |
||||||
|
c = (int)msg.lParam; |
||||||
|
break; |
||||||
|
} |
||||||
|
TranslateMessage(&msg); |
||||||
|
DispatchMessage(&msg); |
||||||
|
result.append((Int)type); |
||||||
|
result.append((Int)a); |
||||||
|
result.append((Int)b); |
||||||
|
result.append((Int)c); |
||||||
|
return result; |
||||||
|
} |
||||||
|
return Array(); |
||||||
|
} |
||||||
|
|
||||||
|
Int php_win_get_tick_count() { |
||||||
|
return (Int)GetTickCount(); |
||||||
|
} |
||||||
|
|
||||||
|
Int php_win_message_box(Int hWnd, String text, String caption, Int uType) { |
||||||
|
int wtext_len = MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, NULL, 0); |
||||||
|
wchar_t* wtext = new wchar_t[wtext_len]; |
||||||
|
MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, wtext, wtext_len); |
||||||
|
|
||||||
|
int wcaption_len = MultiByteToWideChar(CP_UTF8, 0, caption.data(), -1, NULL, 0); |
||||||
|
wchar_t* wcaption = new wchar_t[wcaption_len]; |
||||||
|
MultiByteToWideChar(CP_UTF8, 0, caption.data(), -1, wcaption, wcaption_len); |
||||||
|
|
||||||
|
int result = MessageBoxW((HWND)hWnd, wtext, wcaption, (UINT)uType); |
||||||
|
|
||||||
|
delete[] wtext; |
||||||
|
delete[] wcaption; |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_message_beep(Int type) { |
||||||
|
MessageBeep((UINT)type); |
||||||
|
} |
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Double-buffered frame
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Begin a frame. Returns the memory-DC handle (an Int) used by all draw calls.
|
||||||
|
Int php_win_begin_paint(Int hWnd) { |
||||||
|
HDC hdc = GetDC((HWND)hWnd); |
||||||
|
RECT rc; |
||||||
|
GetClientRect((HWND)hWnd, &rc); |
||||||
|
|
||||||
|
HDC memDC = CreateCompatibleDC(hdc); |
||||||
|
HBITMAP memBitmap = CreateCompatibleBitmap(hdc, rc.right, rc.bottom); |
||||||
|
HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap); |
||||||
|
FrameState state; |
||||||
|
state.bitmap = memBitmap; |
||||||
|
state.oldBitmap = oldBitmap; |
||||||
|
g_frames[memDC] = state; |
||||||
|
|
||||||
|
ReleaseDC((HWND)hWnd, hdc); |
||||||
|
return (Int)memDC; |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_end_paint(Int hWnd, Int hdcHandle) { |
||||||
|
HDC memDC = (HDC)hdcHandle; |
||||||
|
RECT rc; |
||||||
|
GetClientRect((HWND)hWnd, &rc); |
||||||
|
|
||||||
|
HDC hdc = GetDC((HWND)hWnd); |
||||||
|
BitBlt(hdc, 0, 0, rc.right, rc.bottom, memDC, 0, 0, SRCCOPY); |
||||||
|
ReleaseDC((HWND)hWnd, hdc); |
||||||
|
|
||||||
|
auto it = g_frames.find(memDC); |
||||||
|
if (it != g_frames.end()) { |
||||||
|
SelectObject(memDC, it->second.oldBitmap); |
||||||
|
DeleteObject(it->second.bitmap); |
||||||
|
g_frames.erase(it); |
||||||
|
} |
||||||
|
DeleteDC(memDC); |
||||||
|
} |
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// GDI primitives
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void php_win_fill_rect(Int hdc, Int x, Int y, Int w, Int h, Int rgbColor) { |
||||||
|
HBRUSH brush = CreateSolidBrush((COLORREF)rgbColor); |
||||||
|
RECT r = {(int)x, (int)y, (int)(x + w), (int)(y + h)}; |
||||||
|
FillRect((HDC)hdc, &r, brush); |
||||||
|
DeleteObject(brush); |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_draw_block(Int hdc, Int x, Int y, Int size, Int rgbColor) { |
||||||
|
COLORREF color = (COLORREF)rgbColor; |
||||||
|
HBRUSH brush = CreateSolidBrush(color); |
||||||
|
RECT r = {(int)x + 1, (int)y + 1, (int)(x + size - 1), (int)(y + size - 1)}; |
||||||
|
FillRect((HDC)hdc, &r, brush); |
||||||
|
DeleteObject(brush); |
||||||
|
HPEN borderPen = CreatePen(PS_SOLID, 1, RGB( |
||||||
|
(BYTE)(GetRValue(color) * 0.6), |
||||||
|
(BYTE)(GetGValue(color) * 0.6), |
||||||
|
(BYTE)(GetBValue(color) * 0.6))); |
||||||
|
HPEN oldPen = (HPEN)SelectObject((HDC)hdc, borderPen); |
||||||
|
HBRUSH oldBrush = (HBRUSH)SelectObject((HDC)hdc, GetStockObject(NULL_BRUSH)); |
||||||
|
Rectangle((HDC)hdc, (int)x, (int)y, (int)(x + size), (int)(y + size)); |
||||||
|
SelectObject((HDC)hdc, oldBrush); |
||||||
|
SelectObject((HDC)hdc, oldPen); |
||||||
|
DeleteObject(borderPen); |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_draw_line(Int hdc, Int x1, Int y1, Int x2, Int y2, Int rgbColor) { |
||||||
|
HPEN pen = CreatePen(PS_SOLID, 1, (COLORREF)rgbColor); |
||||||
|
HPEN oldPen = (HPEN)SelectObject((HDC)hdc, pen); |
||||||
|
MoveToEx((HDC)hdc, (int)x1, (int)y1, NULL); |
||||||
|
LineTo((HDC)hdc, (int)x2, (int)y2); |
||||||
|
SelectObject((HDC)hdc, oldPen); |
||||||
|
DeleteObject(pen); |
||||||
|
} |
||||||
|
|
||||||
|
// Ellipse using CENTER coordinate semantics (matches libui's fillEllipse).
|
||||||
|
void php_win_fill_ellipse(Int hdc, Int cx, Int cy, Int w, Int h, Int rgbColor) { |
||||||
|
HBRUSH brush = CreateSolidBrush((COLORREF)rgbColor); |
||||||
|
HBRUSH oldBrush = (HBRUSH)SelectObject((HDC)hdc, brush); |
||||||
|
HPEN pen = CreatePen(PS_SOLID, 1, (COLORREF)rgbColor); |
||||||
|
HPEN oldPen = (HPEN)SelectObject((HDC)hdc, pen); |
||||||
|
Ellipse((HDC)hdc, |
||||||
|
(int)(cx - w / 2), (int)(cy - h / 2), |
||||||
|
(int)(cx + w / 2), (int)(cy + h / 2)); |
||||||
|
SelectObject((HDC)hdc, oldPen); |
||||||
|
SelectObject((HDC)hdc, oldBrush); |
||||||
|
DeleteObject(pen); |
||||||
|
DeleteObject(brush); |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_fill_rounded_rect(Int hdc, Int x, Int y, Int w, Int h, Int radius, Int rgbColor) { |
||||||
|
HBRUSH brush = CreateSolidBrush((COLORREF)rgbColor); |
||||||
|
HPEN nullPen = (HPEN)GetStockObject(NULL_PEN); |
||||||
|
HBRUSH oldBrush = (HBRUSH)SelectObject((HDC)hdc, brush); |
||||||
|
HPEN oldPen = (HPEN)SelectObject((HDC)hdc, nullPen); |
||||||
|
RoundRect((HDC)hdc, (int)x, (int)y, (int)(x + w), (int)(y + h), |
||||||
|
(int)radius * 2, (int)radius * 2); |
||||||
|
SelectObject((HDC)hdc, oldPen); |
||||||
|
SelectObject((HDC)hdc, oldBrush); |
||||||
|
DeleteObject(brush); |
||||||
|
} |
||||||
|
|
||||||
|
void php_win_stroke_rounded_rect(Int hdc, Int x, Int y, Int w, Int h, Int radius, Int rgbColor, Int thickness) { |
||||||
|
HPEN pen = CreatePen(PS_SOLID, (int)thickness, (COLORREF)rgbColor); |
||||||
|
HBRUSH nullBrush = (HBRUSH)GetStockObject(NULL_BRUSH); |
||||||
|
HPEN oldPen = (HPEN)SelectObject((HDC)hdc, pen); |
||||||
|
HBRUSH oldBrush = (HBRUSH)SelectObject((HDC)hdc, nullBrush); |
||||||
|
RoundRect((HDC)hdc, (int)x, (int)y, (int)(x + w), (int)(y + h), |
||||||
|
(int)radius * 2, (int)radius * 2); |
||||||
|
SelectObject((HDC)hdc, oldBrush); |
||||||
|
SelectObject((HDC)hdc, oldPen); |
||||||
|
DeleteObject(pen); |
||||||
|
} |
||||||
|
|
||||||
|
// Plain ASCII text (kept for parity / simple labels).
|
||||||
|
void php_win_draw_text(Int hdc, Int x, Int y, String text, Int fontSize, Int rgbColor, Int bold) { |
||||||
|
SetTextColor((HDC)hdc, (COLORREF)rgbColor); |
||||||
|
SetBkMode((HDC)hdc, TRANSPARENT); |
||||||
|
HFONT hFont = CreateFontA((int)fontSize, 0, 0, 0, |
||||||
|
bold ? FW_BOLD : FW_NORMAL, FALSE, FALSE, FALSE, |
||||||
|
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, |
||||||
|
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, "Arial"); |
||||||
|
HFONT oldFont = (HFONT)SelectObject((HDC)hdc, hFont); |
||||||
|
TextOutA((HDC)hdc, (int)x, (int)y, text.data(), (int)strlen(text.data())); |
||||||
|
SelectObject((HDC)hdc, oldFont); |
||||||
|
DeleteObject(hFont); |
||||||
|
} |
||||||
|
|
||||||
|
// UTF-8 text with left/center/right alignment inside an optional width box.
|
||||||
|
// align: 0 = left, 1 = center, 2 = right
|
||||||
|
void php_win_draw_text_ex(Int hdc, Int x, Int y, String text, Int fontSize, Int rgbColor, Int bold, Int width, Int align) { |
||||||
|
SetTextColor((HDC)hdc, (COLORREF)rgbColor); |
||||||
|
SetBkMode((HDC)hdc, TRANSPARENT); |
||||||
|
|
||||||
|
int wtext_len = MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, NULL, 0); |
||||||
|
wchar_t* wtext = new wchar_t[wtext_len]; |
||||||
|
MultiByteToWideChar(CP_UTF8, 0, text.data(), -1, wtext, wtext_len); |
||||||
|
|
||||||
|
HFONT hFont = CreateFontW((int)fontSize, 0, 0, 0, |
||||||
|
bold ? FW_BOLD : FW_NORMAL, FALSE, FALSE, FALSE, |
||||||
|
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, |
||||||
|
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, L"Microsoft YaHei"); |
||||||
|
HFONT oldFont = (HFONT)SelectObject((HDC)hdc, hFont); |
||||||
|
|
||||||
|
int drawX = (int)x; |
||||||
|
if (width > 0) { |
||||||
|
SIZE sz; |
||||||
|
GetTextExtentPoint32W((HDC)hdc, wtext, (int)wcslen(wtext), &sz); |
||||||
|
if (align == 1) { |
||||||
|
drawX = (int)x + ((int)width - sz.cx) / 2; |
||||||
|
} else if (align == 2) { |
||||||
|
drawX = (int)x + (int)width - sz.cx; |
||||||
|
} |
||||||
|
} |
||||||
|
TextOutW((HDC)hdc, drawX, (int)y, wtext, (int)wcslen(wtext)); |
||||||
|
|
||||||
|
SelectObject((HDC)hdc, oldFont); |
||||||
|
DeleteObject(hFont); |
||||||
|
delete[] wtext; |
||||||
|
} |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** |
||||||
|
* 海贼王 · 斗地主 —— TypePHP / Win32 入口。 |
||||||
|
* |
||||||
|
* 本文件替代原 libui 版 onepiece-doudizhu.php 的入口:不再依赖 libui 事件循环, |
||||||
|
* 而是用 Win32 消息循环驱动 GameController(见 php-src/doudizhu/GameController.php)。 |
||||||
|
* 所有游戏逻辑(发牌 / 叫地主 / 出牌 / AI / 技能 / 渲染)均在 PHP 中实现, |
||||||
|
* C++ 仅负责窗口、GDI 绘制原语与输入(cpp-src/win32.cc)。 |
||||||
|
* |
||||||
|
* 编译:在 examples/onepiece-doudizhu-win32 目录执行 |
||||||
|
* tpc.exe project.yml |
||||||
|
* 运行:生成的 .exe(无外部 PHP 依赖,纯 Win32 + GDI)。 |
||||||
|
* |
||||||
|
* 注意:TypePHP 的 bin 模式会自动以 main() 作为程序入口,无需手动调用。 |
||||||
|
*/ |
||||||
|
|
||||||
|
// Win32 常量 |
||||||
|
const SW_SHOW = 5; |
||||||
|
|
||||||
|
// win_peek_message() 返回的消息类型 |
||||||
|
const MSG_OTHER = 0; |
||||||
|
const MSG_MOUSE_DOWN = 1; |
||||||
|
const MSG_MOUSE_UP = 2; |
||||||
|
const MSG_MOUSE_MOVE = 3; |
||||||
|
const MSG_KEY_DOWN = 4; |
||||||
|
|
||||||
|
use Yangweijie\Ui2\Games\OnePieceDoudizhu\GameController; |
||||||
|
use Yangweijie\Ui2\Games\OnePieceDoudizhu\Sound; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
\date_default_timezone_set('Asia/Shanghai'); |
||||||
|
\Yangweijie\Ui2\Games\OnePieceDoudizhu\ensureDdzFont(); |
||||||
|
|
||||||
|
$hWnd = win_create_window('海贼王 · 斗地主', GameController::WIN_W, GameController::WIN_H); |
||||||
|
if ($hWnd == 0) { |
||||||
|
echo "窗口创建失败!\n"; |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
win_show_window($hWnd, SW_SHOW); |
||||||
|
|
||||||
|
$ctrl = new GameController(); |
||||||
|
$ctrl->hWnd = $hWnd; |
||||||
|
$ctrl->newGame(); |
||||||
|
|
||||||
|
echo "海贼王 · 斗地主 已启动(Win32 / TypePHP)\n"; |
||||||
|
echo "提示:拖拽手牌选牌,底部按钮出牌/不出/提示/技能/托管,右上角切换音效。\n"; |
||||||
|
|
||||||
|
while (true) { |
||||||
|
// 1) 处理所有待处理消息 |
||||||
|
while (true) { |
||||||
|
$m = win_peek_message(); |
||||||
|
if (\count($m) === 0) { |
||||||
|
break; |
||||||
|
} |
||||||
|
$type = $m[0] ?? MSG_OTHER; |
||||||
|
if ($type === MSG_MOUSE_DOWN) { |
||||||
|
$ctrl->onMouse((object) ['x' => $m[1], 'y' => $m[2], 'down' => 1, 'up' => 0, 'held' => 0]); |
||||||
|
} elseif ($type === MSG_MOUSE_UP) { |
||||||
|
$ctrl->onMouse((object) ['x' => $m[1], 'y' => $m[2], 'down' => 0, 'up' => 1, 'held' => 0]); |
||||||
|
} elseif ($type === MSG_MOUSE_MOVE) { |
||||||
|
$ctrl->onMouse((object) ['x' => $m[1], 'y' => $m[2], 'down' => 0, 'up' => 0, 'held' => $m[3]]); |
||||||
|
} elseif ($type === MSG_KEY_DOWN) { |
||||||
|
$ctrl->onKey((int) $m[1]); |
||||||
|
} |
||||||
|
// MSG_OTHER (例如 WM_PAINT) 已在 WndProc 中 ValidateRect,此处忽略 |
||||||
|
} |
||||||
|
|
||||||
|
if (win_quit_requested()) { |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// 2) 触发到期的定时器(AI 走子 / 叫分 / 托管自动出牌) |
||||||
|
$ctrl->tick(); |
||||||
|
|
||||||
|
// 3) 渲染当前帧(双缓冲,整窗重绘) |
||||||
|
$ctrl->render(); |
||||||
|
|
||||||
|
// 4) 简单节流到 ~60 FPS |
||||||
|
\usleep(16000); |
||||||
|
} |
||||||
|
|
||||||
|
Sound::instance()->unload(); |
||||||
|
echo "游戏结束,bye!\n"; |
||||||
|
} |
||||||
@ -0,0 +1,252 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* AI 对手:手牌评估 + 跟牌/首出启发式 + 势力感知行为 + 技能效用决策。 |
||||||
|
* |
||||||
|
* 设计为「无状态决策器」:每次轮到 AI 时调用 act() 完成整个回合动作。 |
||||||
|
* 赤犬反击(counterBomb)属于反应式,由驱动器在对手出炸弹后单独调用 maybeCounter()。 |
||||||
|
*/ |
||||||
|
final class Ai |
||||||
|
{ |
||||||
|
/** 叫分:按手牌强度评估 0..3。 */ |
||||||
|
public static function bid(array $hand): int |
||||||
|
{ |
||||||
|
$score = 0; |
||||||
|
$byRank = []; |
||||||
|
foreach ($hand as $c) { |
||||||
|
$byRank[$c->rank] = ($byRank[$c->rank] ?? 0) + 1; |
||||||
|
} |
||||||
|
foreach ($byRank as $r => $n) { |
||||||
|
if ($n === 4) { |
||||||
|
$score += 7; // 炸弹 |
||||||
|
} |
||||||
|
if ($r === Card::JOKER_SMALL) { |
||||||
|
$score += 4; |
||||||
|
} |
||||||
|
if ($r === Card::JOKER_BIG) { |
||||||
|
$score += 5; |
||||||
|
} |
||||||
|
if ($r === 15) { |
||||||
|
$score += 2; // 2 |
||||||
|
} |
||||||
|
if ($r === 14 || $r === 13) { |
||||||
|
$score += 1; // A/K |
||||||
|
} |
||||||
|
} |
||||||
|
if ($score >= 16) { |
||||||
|
return 3; |
||||||
|
} |
||||||
|
if ($score >= 10) { |
||||||
|
return 2; |
||||||
|
} |
||||||
|
if ($score >= 6) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
|
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
/** 执行 AI 玩家的整个回合(含技能)。 */ |
||||||
|
public static function act(Game $g, int $player): void |
||||||
|
{ |
||||||
|
if ($g->phase !== 'playing' || $g->turn !== $player) { |
||||||
|
return; |
||||||
|
} |
||||||
|
self::maybeTurnStartSkill($g, $player); |
||||||
|
if ($g->phase !== 'playing' || $g->turn !== $player) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$moves = $g->legalMoves($player); |
||||||
|
if ($g->lastPlay === null) { |
||||||
|
if ($moves === []) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$mv = self::pickLead($g, $player, $moves); |
||||||
|
self::maybeArmOnPlay($g, $player, $mv); |
||||||
|
self::maybeArmOnBomb($g, $player, $mv); |
||||||
|
$g->play($player, $mv->cards); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$beat = self::pickFollow($g, $player, $moves); |
||||||
|
if ($beat === null) { |
||||||
|
$g->pass($player); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
self::maybeArmOnPlay($g, $player, $beat); |
||||||
|
self::maybeArmOnBomb($g, $player, $beat); |
||||||
|
$g->play($player, $beat->cards); |
||||||
|
} |
||||||
|
|
||||||
|
/** 赤犬反击:对手刚出炸弹时调用,决定是否反击。 */ |
||||||
|
public static function maybeCounter(Game $g, int $player): bool |
||||||
|
{ |
||||||
|
if ($g->lastPlay === null || !$g->lastPlay['combo']->isBomb) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($g->players[$player]->characterId !== 'akainu') { |
||||||
|
return false; |
||||||
|
} |
||||||
|
$skill = Character::byId('akainu')->skill(); |
||||||
|
if (!$g->canUse($player, $skill)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
$bomber = $g->lastPlay['player']; |
||||||
|
// 农民反击地主,或地主反击农民(阻止其快出完) |
||||||
|
$threat = $g->handCount($bomber) <= 5; |
||||||
|
if ($g->side($player) !== $g->side($bomber) || $threat) { |
||||||
|
$g->counterBomb($player); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 决策细节 |
||||||
|
/** |
||||||
|
* 首出时挑选要甩出的牌组(公开,供控制器提示与测试复用)。 |
||||||
|
* |
||||||
|
* @param list<Combo> $moves |
||||||
|
*/ |
||||||
|
public static function pickLead(Game $g, int $player, array $moves): Combo |
||||||
|
{ |
||||||
|
$nonBomb = \array_filter($moves, static fn (Combo $c) => !$c->isBomb && !$c->isRocket); |
||||||
|
if ($nonBomb !== []) { |
||||||
|
// 优先甩出牌数多的组合(顺/飞机/连对),其次最小单 |
||||||
|
\usort($nonBomb, static function (Combo $a, Combo $b): int { |
||||||
|
$ca = \count($a->cards); |
||||||
|
$cb = \count($b->cards); |
||||||
|
if ($cb !== $ca) { |
||||||
|
return $cb <=> $ca; |
||||||
|
} |
||||||
|
|
||||||
|
return $a->rank <=> $b->rank; |
||||||
|
}); |
||||||
|
|
||||||
|
return $nonBomb[0]; |
||||||
|
} |
||||||
|
// 只剩炸弹/火箭 |
||||||
|
\usort($moves, static fn (Combo $a, Combo $b): int => $a->rank <=> $b->rank); |
||||||
|
|
||||||
|
return $moves[0]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 跟牌时挑选要压过的牌组(公开,供控制器提示与测试复用)。 |
||||||
|
* |
||||||
|
* @param list<Combo> $moves |
||||||
|
*/ |
||||||
|
public static function pickFollow(Game $g, int $player, array $moves): ?Combo |
||||||
|
{ |
||||||
|
if ($moves === []) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$oppHand = PHP_INT_MAX; |
||||||
|
foreach ([0, 1, 2] as $p) { |
||||||
|
if ($p !== $player && $g->side($p) !== $g->side($player)) { |
||||||
|
$oppHand = \min($oppHand, $g->handCount($p)); |
||||||
|
} |
||||||
|
} |
||||||
|
$nonBomb = \array_filter($moves, static fn (Combo $c) => !$c->isBomb && !$c->isRocket); |
||||||
|
if ($nonBomb !== []) { |
||||||
|
\usort($nonBomb, static fn (Combo $a, Combo $b): int => $a->rank <=> $b->rank); |
||||||
|
|
||||||
|
return $nonBomb[0]; |
||||||
|
} |
||||||
|
// 只有炸弹/火箭:对手快出完才用 |
||||||
|
if ($oppHand <= 2) { |
||||||
|
\usort($moves, static fn (Combo $a, Combo $b): int => $a->rank <=> $b->rank); |
||||||
|
|
||||||
|
return $moves[0]; |
||||||
|
} |
||||||
|
|
||||||
|
return null; // 留炸弹 |
||||||
|
} |
||||||
|
|
||||||
|
private static function maybeTurnStartSkill(Game $g, int $player): void |
||||||
|
{ |
||||||
|
$ps = $g->players[$player]; |
||||||
|
if ($ps->characterId === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$skill = Character::byId($ps->characterId)->skill(); |
||||||
|
if ($skill->trigger !== 'onTurnStart') { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!$g->canUse($player, $skill)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 目标:手牌最少的对手(最具威胁) |
||||||
|
$target = null; |
||||||
|
$best = PHP_INT_MAX; |
||||||
|
foreach ([0, 1, 2] as $p) { |
||||||
|
if ($p === $player) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$c = $g->handCount($p); |
||||||
|
if ($c < $best) { |
||||||
|
$best = $c; |
||||||
|
$target = $p; |
||||||
|
} |
||||||
|
} |
||||||
|
if ($target === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 势力感知:海军/七武海更爱用控制技;四皇(大妈)偷牌也积极 |
||||||
|
$g->armSkill($player, $target); |
||||||
|
} |
||||||
|
|
||||||
|
private static function maybeArmOnPlay(Game $g, int $player, Combo $mv): void |
||||||
|
{ |
||||||
|
$ps = $g->players[$player]; |
||||||
|
if ($ps->characterId === null || $ps->armed !== null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$skill = Character::byId($ps->characterId)->skill(); |
||||||
|
if ($skill->trigger !== 'onPlay') { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!$g->canUse($player, $skill)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$faction = $ps->faction; |
||||||
|
if ($skill->id === 'garp_shock' || $skill->id === 'mihawk_unblock') { |
||||||
|
if ($mv->type === 'single' || $mv->type === 'pair') { |
||||||
|
// 海军/七武海:用控制技压制 |
||||||
|
$g->armSkill($player); |
||||||
|
} |
||||||
|
} elseif ($skill->id === 'shanks_haki') { |
||||||
|
// 四皇:手牌少时花霸气锁 trick;否则偶尔用 |
||||||
|
if ($g->handCount($player) <= 6 || ($g->lastPlay !== null && \rand(0, 2) === 0)) { |
||||||
|
$g->armSkill($player); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static function maybeArmOnBomb(Game $g, int $player, Combo $mv): void |
||||||
|
{ |
||||||
|
$ps = $g->players[$player]; |
||||||
|
if ($ps->characterId === null || $ps->armed !== null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$skill = Character::byId($ps->characterId)->skill(); |
||||||
|
if ($skill->trigger !== 'onBombPlayed' || !$mv->isBomb) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (!$g->canUse($player, $skill)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if ($skill->id === 'whitebeard_quake') { |
||||||
|
$g->armSkill($player); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* 一张牌:rank 决定牌力,suit 仅用于显示与花色技能(四皇「领土宣言」)。 |
||||||
|
* |
||||||
|
* 牌力序(升序): |
||||||
|
* 3..10 -> 3..10 |
||||||
|
* J=11 Q=12 K=13 A=14 2=15 |
||||||
|
* 小王=16 大王=17 |
||||||
|
*/ |
||||||
|
final class Card |
||||||
|
{ |
||||||
|
public const SUITS = ['♠', '♥', '♦', '♣']; // ♠ ♥ ♦ ♣ |
||||||
|
public const JOKER_SMALL = 16; |
||||||
|
public const JOKER_BIG = 17; |
||||||
|
|
||||||
|
private const RANK_LABELS = [ |
||||||
|
3 => '3', 4 => '4', 5 => '5', 6 => '6', 7 => '7', 8 => '8', 9 => '9', 10 => '10', |
||||||
|
11 => 'J', 12 => 'Q', 13 => 'K', 14 => 'A', 15 => '2', |
||||||
|
16 => '小王', 17 => '大王', |
||||||
|
]; |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
public int $rank, |
||||||
|
public string $suit = '', |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public static function smallJoker(): self |
||||||
|
{ |
||||||
|
return new self(self::JOKER_SMALL, 'JOKER'); |
||||||
|
} |
||||||
|
|
||||||
|
public static function bigJoker(): self |
||||||
|
{ |
||||||
|
return new self(self::JOKER_BIG, 'JOKER'); |
||||||
|
} |
||||||
|
|
||||||
|
/** 人类可读标签,如「♠A」「大王」。 */ |
||||||
|
public function label(): string |
||||||
|
{ |
||||||
|
if ($this->rank >= self::JOKER_SMALL) { |
||||||
|
return self::RANK_LABELS[$this->rank]; |
||||||
|
} |
||||||
|
|
||||||
|
return $this->suit . self::RANK_LABELS[$this->rank]; |
||||||
|
} |
||||||
|
|
||||||
|
/** 稳定 id,用于去重/比较(同点不同花色算不同牌)。 */ |
||||||
|
public function id(): string |
||||||
|
{ |
||||||
|
return $this->suit . ':' . $this->rank; |
||||||
|
} |
||||||
|
|
||||||
|
public function isJoker(): bool |
||||||
|
{ |
||||||
|
return $this->rank >= self::JOKER_SMALL; |
||||||
|
} |
||||||
|
|
||||||
|
public function jsonSerialize(): array |
||||||
|
{ |
||||||
|
return ['rank' => $this->rank, 'suit' => $this->suit, 'label' => $this->label()]; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,90 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* 9 名代表性角色(每势力 3 名),各带一个独特技能。 |
||||||
|
* 技能深度结合卡牌对战:onPlay/onBombPlayed 挂出牌修正,onTurnStart 直接施加战场状态。 |
||||||
|
*/ |
||||||
|
final class Character |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public string $id, |
||||||
|
public string $name, |
||||||
|
public string $title, |
||||||
|
public string $faction, |
||||||
|
public string $skillId, |
||||||
|
public string $skillName, |
||||||
|
public string $skillDesc, |
||||||
|
public string $skillTrigger, |
||||||
|
public string $skillCost, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public function skill(): Skill |
||||||
|
{ |
||||||
|
return new Skill( |
||||||
|
$this->skillId, |
||||||
|
$this->skillName, |
||||||
|
$this->skillDesc, |
||||||
|
$this->skillTrigger, |
||||||
|
$this->skillCost, |
||||||
|
$this->faction, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<self> */ |
||||||
|
public static function all(): array |
||||||
|
{ |
||||||
|
return [ |
||||||
|
// 海军本部 — 控制 / 反制 |
||||||
|
new self('garp', '蒙奇·D·卡普', '拳骨', Faction::NAVY, 'garp_shock', '银河碎拳', |
||||||
|
'出单/对时启用:该手牌附带冲击波,下家本 trick 强制 pass。', 'onPlay', 'once'), |
||||||
|
new self('akainu', '萨卡斯基', '冥狗', Faction::NAVY, 'akainu_magma', '岩浆灼烧', |
||||||
|
'对手出炸弹时反击:使其炸弹无效,并禁用其下个炸弹。', 'onBombPlayed', 'charges:2'), |
||||||
|
new self('aokiji', '库赞', '冰河时代', Faction::NAVY, 'aokiji_freeze', '冻结', |
||||||
|
'冻结一名对手 1 回合(自动 pass)。', 'onTurnStart', 'once'), |
||||||
|
|
||||||
|
// 王下七武海 — 诡诈 / 交换 |
||||||
|
new self('mihawk', '鹰眼 米霍克', '世界第一大剑豪', Faction::WARLORD, 'mihawk_unblock', '黑刀·夜', |
||||||
|
'出单/对时启用:该手牌不可被非炸拦截。', 'onPlay', 'charges:2'), |
||||||
|
new self('boa', '波雅·汉库克', '女帝', Faction::WARLORD, 'boa_petrify', '虏之矢', |
||||||
|
'石化一名对手 2 回合(禁用其技能)。', 'onTurnStart', 'once'), |
||||||
|
new self('kuma', '巴索罗米·熊', '暴君', Faction::WARLORD, 'kuma_push', '肉球推送', |
||||||
|
'将你最小的一张手牌推给指定对手(扰乱牌型)。', 'onTurnStart', 'once'), |
||||||
|
|
||||||
|
// 四皇 — 压制 / 掠夺 |
||||||
|
new self('shanks', '香克斯', '红发', Faction::EMPEROR, 'shanks_haki', '霸王色霸气', |
||||||
|
'出牌时花费 1 霸气:该手牌凌驾一切非炸(除非炸弹/火箭)。', 'onPlay', 'haki:1'), |
||||||
|
new self('whitebeard', '爱德华·纽盖特', '白胡子', Faction::EMPEROR, 'whitebeard_quake', '震震果实', |
||||||
|
'出炸弹时启用:炸弹威力 +1(视为更大炸弹)。', 'onBombPlayed', 'charges:2'), |
||||||
|
new self('bigmom', '夏洛特·玲玲', '大妈', Faction::EMPEROR, 'bigmom_steal', '魂魂召唤', |
||||||
|
'随机偷取一名对手 1 张手牌。', 'onTurnStart', 'once'), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function byId(string $id): self |
||||||
|
{ |
||||||
|
static $map = null; |
||||||
|
if ($map === null) { |
||||||
|
$map = []; |
||||||
|
foreach (self::all() as $c) { |
||||||
|
$map[$c->id] = $c; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (!isset($map[$id])) { |
||||||
|
throw new \InvalidArgumentException("未知角色: $id"); |
||||||
|
} |
||||||
|
|
||||||
|
return $map[$id]; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<self> */ |
||||||
|
public static function byFaction(string $faction): array |
||||||
|
{ |
||||||
|
return \array_values(\array_filter(self::all(), static fn (self $c) => $c->faction === $faction)); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,252 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
use RuntimeException; |
||||||
|
|
||||||
|
/** |
||||||
|
* 牌型识别与比较。 |
||||||
|
* |
||||||
|
* 支持:单/对/三/三带一/三带二/顺子/连对/飞机(含翼)/炸弹/火箭。 |
||||||
|
* 比较规则:火箭 > 炸弹(比点) > 普通(同型同长比点)。技能修正(霸气/不可拦截) |
||||||
|
* 由 Engine 在对局层处理,不在本类内。 |
||||||
|
*/ |
||||||
|
final class Combo |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public string $type, |
||||||
|
public int $rank, // 主要比较点(炸弹/火箭用 17) |
||||||
|
public int $length, // 顺子/连对/飞机的长度 |
||||||
|
public array $cards, // list<Card> |
||||||
|
public bool $isBomb = false, |
||||||
|
public bool $isRocket = false, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<Card> $cards |
||||||
|
*/ |
||||||
|
public static function parse(array $cards): ?self |
||||||
|
{ |
||||||
|
$n = \count($cards); |
||||||
|
if ($n === 0) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
$byRank = []; |
||||||
|
foreach ($cards as $c) { |
||||||
|
$byRank[$c->rank][] = $c; |
||||||
|
} |
||||||
|
$ranks = \array_keys($byRank); |
||||||
|
\sort($ranks); |
||||||
|
$counts = []; |
||||||
|
foreach ($ranks as $r) { |
||||||
|
$counts[$r] = \count($byRank[$r]); |
||||||
|
} |
||||||
|
|
||||||
|
// 火箭(双王) |
||||||
|
if ($n === 2 && isset($byRank[Card::JOKER_SMALL]) && isset($byRank[Card::JOKER_BIG])) { |
||||||
|
return new self('rocket', Card::JOKER_BIG, 1, $cards, false, true); |
||||||
|
} |
||||||
|
|
||||||
|
// 炸弹(四同) |
||||||
|
if ($n === 4 && \count($ranks) === 1) { |
||||||
|
return new self('bomb', $ranks[0], 1, $cards, true); |
||||||
|
} |
||||||
|
|
||||||
|
// 单 |
||||||
|
if ($n === 1) { |
||||||
|
return new self('single', $ranks[0], 1, $cards); |
||||||
|
} |
||||||
|
|
||||||
|
// 对 |
||||||
|
if ($n === 2 && \count($ranks) === 1) { |
||||||
|
return new self('pair', $ranks[0], 1, $cards); |
||||||
|
} |
||||||
|
|
||||||
|
// 三 |
||||||
|
if ($n === 3 && \count($ranks) === 1) { |
||||||
|
return new self('triple', $ranks[0], 1, $cards); |
||||||
|
} |
||||||
|
|
||||||
|
// 三带一 |
||||||
|
if ($n === 4 && \count($ranks) === 2) { |
||||||
|
$triple = self::rankWithCount($counts, 3); |
||||||
|
if ($triple !== null) { |
||||||
|
return new self('triple1', $triple, 1, $cards); |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// 三带二 |
||||||
|
if ($n === 5 && \count($ranks) === 2) { |
||||||
|
$triple = self::rankWithCount($counts, 3); |
||||||
|
$pair = self::rankWithCount($counts, 2); |
||||||
|
if ($triple !== null && $pair !== null) { |
||||||
|
return new self('triple2', $triple, 1, $cards); |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
$maxRank = \max($ranks); |
||||||
|
$allOnes = \min($counts) === 1 && \max($counts) === 1; |
||||||
|
$allTwos = \min($counts) === 2 && \max($counts) === 2; |
||||||
|
$allThrees = \min($counts) === 3 && \max($counts) === 3; |
||||||
|
|
||||||
|
// 顺子(≥5 连续单张,不含 2/王) |
||||||
|
if ($allOnes && $n >= 5 && $maxRank <= 14 && self::isConsecutive($ranks)) { |
||||||
|
return new self('straight', $maxRank, $n, $cards); |
||||||
|
} |
||||||
|
|
||||||
|
// 连对(≥3 连续对子,不含 2/王) |
||||||
|
if ($allTwos && $n >= 6 && $maxRank <= 14 && self::isConsecutive($ranks)) { |
||||||
|
return new self('straight2', $maxRank, (int) ($n / 2), $cards); |
||||||
|
} |
||||||
|
|
||||||
|
// 飞机(≥2 连续三张,可带翼) |
||||||
|
if ($allThrees) { |
||||||
|
return new self('plane', $maxRank, $n, $cards); |
||||||
|
} |
||||||
|
$plane = self::planeWithWings($ranks, $counts, $n, $cards); |
||||||
|
if ($plane !== null) { |
||||||
|
return $plane; |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param array<int,int> $counts |
||||||
|
*/ |
||||||
|
private static function rankWithCount(array $counts, int $want): ?int |
||||||
|
{ |
||||||
|
foreach ($counts as $r => $c) { |
||||||
|
if ($c === $want) { |
||||||
|
return $r; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<int> $ranks |
||||||
|
* @param array<int,int> $counts |
||||||
|
* @param list<Card> $cards |
||||||
|
*/ |
||||||
|
private static function planeWithWings(array $ranks, array $counts, int $n, array $cards): ?self |
||||||
|
{ |
||||||
|
$tripleRanks = []; |
||||||
|
$wingRanks = []; |
||||||
|
foreach ($ranks as $r) { |
||||||
|
if ($counts[$r] === 3) { |
||||||
|
$tripleRanks[] = $r; |
||||||
|
} else { |
||||||
|
$wingRanks[] = $r; |
||||||
|
} |
||||||
|
} |
||||||
|
if (\count($tripleRanks) < 2 || !self::isConsecutive($tripleRanks) || \max($tripleRanks) > 14) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$t = \count($tripleRanks); |
||||||
|
$expectedWings = $n - 3 * $t; |
||||||
|
|
||||||
|
// 无翼 |
||||||
|
if ($expectedWings === 0) { |
||||||
|
return new self('plane', \max($tripleRanks), $t, $cards); |
||||||
|
} |
||||||
|
// 单翼:t 张单牌 |
||||||
|
if ($expectedWings === $t && \count($wingRanks) === $t && self::allCount($wingRanks, $counts, 1)) { |
||||||
|
return new self('plane1', \max($tripleRanks), $t, $cards); |
||||||
|
} |
||||||
|
// 对翼:t 个对子 |
||||||
|
if ($expectedWings === 2 * $t && \count($wingRanks) === $t && self::allCount($wingRanks, $counts, 2)) { |
||||||
|
return new self('plane2', \max($tripleRanks), $t, $cards); |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<int> $ranks |
||||||
|
* @param array<int,int> $counts |
||||||
|
*/ |
||||||
|
private static function allCount(array $ranks, array $counts, int $want): bool |
||||||
|
{ |
||||||
|
foreach ($ranks as $r) { |
||||||
|
if (($counts[$r] ?? 0) !== $want) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<int> $ranks |
||||||
|
*/ |
||||||
|
private static function isConsecutive(array $ranks): bool |
||||||
|
{ |
||||||
|
$r = \array_values($ranks); |
||||||
|
if (\count($r) < 2) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
\sort($r); |
||||||
|
for ($i = 1; $i < \count($r); $i++) { |
||||||
|
if ($r[$i] !== $r[$i - 1] + 1) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** a 是否能压过当前桌面 lastPlay(b)。b=null 表示自由出牌。 */ |
||||||
|
public static function beats(self $a, ?self $b): bool |
||||||
|
{ |
||||||
|
if ($b === null) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if ($a->isRocket) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
if ($b->isRocket) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($a->isBomb) { |
||||||
|
if ($b->isBomb) { |
||||||
|
return $a->rank > $b->rank; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
if ($b->isBomb) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($a->type !== $b->type) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($a->length !== $b->length) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return $a->rank > $b->rank; |
||||||
|
} |
||||||
|
|
||||||
|
public function describe(): string |
||||||
|
{ |
||||||
|
$names = [ |
||||||
|
'single' => '单张', 'pair' => '对子', 'triple' => '三张', |
||||||
|
'triple1' => '三带一', 'triple2' => '三带二', 'straight' => '顺子', |
||||||
|
'straight2' => '连对', 'plane' => '飞机', 'plane1' => '飞机带单', |
||||||
|
'plane2' => '飞机带对', 'bomb' => '炸弹', 'rocket' => '王炸', |
||||||
|
]; |
||||||
|
$label = $names[$this->type] ?? $this->type; |
||||||
|
|
||||||
|
return \sprintf('%s(%s)', $label, \implode('', \array_map(static fn (Card $c) => $c->label(), $this->cards))); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* 54 张牌组:4 花色 × 3..2 + 双王。洗牌后发 3 手 + 3 张底牌。 |
||||||
|
*/ |
||||||
|
final class Deck |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @return list<Card> |
||||||
|
*/ |
||||||
|
public static function build(): array |
||||||
|
{ |
||||||
|
$cards = []; |
||||||
|
foreach (Card::SUITS as $suit) { |
||||||
|
for ($r = 3; $r <= 15; $r++) { |
||||||
|
$cards[] = new Card($r, $suit); |
||||||
|
} |
||||||
|
} |
||||||
|
$cards[] = Card::smallJoker(); |
||||||
|
$cards[] = Card::bigJoker(); |
||||||
|
|
||||||
|
return $cards; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 洗牌发牌。 |
||||||
|
* |
||||||
|
* @return array{hands: array<int, list<Card>>, bottom: list<Card>} |
||||||
|
*/ |
||||||
|
public static function deal(): array |
||||||
|
{ |
||||||
|
$cards = self::build(); |
||||||
|
\shuffle($cards); |
||||||
|
|
||||||
|
$hands = [[], [], []]; |
||||||
|
for ($i = 0; $i < 51; $i++) { |
||||||
|
$hands[$i % 3][] = $cards[$i]; |
||||||
|
} |
||||||
|
$bottom = \array_slice($cards, 51, 3); |
||||||
|
|
||||||
|
foreach ($hands as $i => $hand) { |
||||||
|
self::sort($hands[$i]); |
||||||
|
} |
||||||
|
|
||||||
|
return ['hands' => $hands, 'bottom' => $bottom]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 按牌力升序排序(同点花色保持稳定)。 |
||||||
|
* |
||||||
|
* @param list<Card> $hand |
||||||
|
*/ |
||||||
|
public static function sort(array &$hand): void |
||||||
|
{ |
||||||
|
\usort($hand, static fn (Card $a, Card $b): int => $a->rank <=> $b->rank); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* 三大势力:海军本部 / 王下七武海 / 四皇。 |
||||||
|
* 每个势力有「特性」描述(影响 AI 行为与部分被动),成员使用各自主动技能。 |
||||||
|
*/ |
||||||
|
final class Faction |
||||||
|
{ |
||||||
|
public const NAVY = 'navy'; // 海军本部 — 绝对正义(控制/反制) |
||||||
|
public const WARLORD = 'warlord'; // 王下七武海 — 被招安的海盗(诡诈/交换) |
||||||
|
public const EMPEROR = 'emperor'; // 四皇 — 新世界霸主(压制/掠夺) |
||||||
|
|
||||||
|
/** @var array<string, array{name:string, creed:string, trait:string, color:string}> */ |
||||||
|
public static array $defs = [ |
||||||
|
self::NAVY => [ |
||||||
|
'name' => '海军本部', |
||||||
|
'creed' => '绝对正义', |
||||||
|
'trait' => '正义铁拳:每局可反制一次对手炸弹;情报:叫完地主可查看底牌。', |
||||||
|
'color' => '#1e3a8a', |
||||||
|
], |
||||||
|
self::WARLORD => [ |
||||||
|
'name' => '王下七武海', |
||||||
|
'creed' => '被招安的海盗', |
||||||
|
'trait' => '协定漏洞:被迫过牌时可改出最小单张续命(2 次);凭实力借:每局交换 1 张牌。', |
||||||
|
'color' => '#7c3aed', |
||||||
|
], |
||||||
|
self::EMPEROR => [ |
||||||
|
'name' => '四皇', |
||||||
|
'creed' => '新世界霸主', |
||||||
|
'trait' => '霸王色霸气:持有霸气 token,出牌时花费使其凌驾一切非炸;召集:每局抽 1 张。', |
||||||
|
'color' => '#b91c1c', |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
public static function name(string $id): string |
||||||
|
{ |
||||||
|
return self::$defs[$id]['name'] ?? $id; |
||||||
|
} |
||||||
|
|
||||||
|
public static function color(string $id): string |
||||||
|
{ |
||||||
|
return self::$defs[$id]['color'] ?? '#444444'; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,501 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
use InvalidArgumentException; |
||||||
|
use RuntimeException; |
||||||
|
|
||||||
|
/** |
||||||
|
* 对局状态 + 规则引擎 + 技能调度。 |
||||||
|
* |
||||||
|
* 阶段:bidding(叫地主) → playing(出牌) → over。 |
||||||
|
* 技能采用「先 arm(激活)后行动」模型: |
||||||
|
* - onTurnStart 技能:arm 时立即生效(冻结/石化/偷牌/推牌) |
||||||
|
* - onPlay / onBombPlayed 技能:arm 后给「下一手牌」附加修正(跳过/不可拦截/霸气/炸弹+1) |
||||||
|
* - 赤犬反击:对手出炸弹后调用 counterBomb() 取消 |
||||||
|
*/ |
||||||
|
final class Game |
||||||
|
{ |
||||||
|
/** @var list<PlayerState> */ |
||||||
|
public array $players; |
||||||
|
public ?int $landlord = null; |
||||||
|
public int $turn = 0; |
||||||
|
|
||||||
|
/** @var array{player:int, combo:Combo, hakiActive:bool, unblockable:bool, bombBonus:int}|null */ |
||||||
|
public ?array $lastPlay = null; |
||||||
|
public int $passes = 0; |
||||||
|
public string $phase = 'bidding'; |
||||||
|
public ?int $winner = null; |
||||||
|
public ?string $winnerSide = null; |
||||||
|
/** @var list<string> */ |
||||||
|
public array $log = []; |
||||||
|
|
||||||
|
/** 战场修正(按目标玩家) */ |
||||||
|
public array $mods = [ |
||||||
|
'skipNext' => [false, false, false], |
||||||
|
'frozen' => [0, 0, 0], |
||||||
|
'petrified' => [0, 0, 0], |
||||||
|
'bombDisabled' => [0, 0, 0], |
||||||
|
]; |
||||||
|
|
||||||
|
/** 叫分状态 */ |
||||||
|
public array $bidding = [ |
||||||
|
'scores' => [0, 0, 0], |
||||||
|
'current' => 0, |
||||||
|
'highest' => 0, |
||||||
|
'highestPlayer' => null, |
||||||
|
'acted' => [false, false, false], |
||||||
|
]; |
||||||
|
|
||||||
|
/** @var list<Card> 底牌(叫完分给地主) */ |
||||||
|
public array $bottom = []; |
||||||
|
/** 是否已把底牌发给地主(用于「情报」查看) */ |
||||||
|
public bool $bottomRevealed = false; |
||||||
|
|
||||||
|
/** 事件回调(音效/UI 钩子):fn(event, payload) */ |
||||||
|
public $onEvent = null; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<PlayerState> $players |
||||||
|
* @param list<Card> $bottom |
||||||
|
*/ |
||||||
|
public function __construct(array $players, array $bottom) |
||||||
|
{ |
||||||
|
$this->players = $players; |
||||||
|
$this->bottom = $bottom; |
||||||
|
$this->turn = 0; |
||||||
|
} |
||||||
|
|
||||||
|
public function side(int $player): string |
||||||
|
{ |
||||||
|
return $player === $this->landlord ? 'landlord' : 'peasant'; |
||||||
|
} |
||||||
|
|
||||||
|
public function isOver(): bool |
||||||
|
{ |
||||||
|
return $this->phase === 'over'; |
||||||
|
} |
||||||
|
|
||||||
|
public function handCount(int $player): int |
||||||
|
{ |
||||||
|
return \count($this->players[$player]->hand); |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 叫地主 |
||||||
|
/** |
||||||
|
* @return 'ok'|'redeal'|'started' |
||||||
|
*/ |
||||||
|
public function bid(int $player, int $score): string |
||||||
|
{ |
||||||
|
if ($this->phase !== 'bidding') { |
||||||
|
throw new RuntimeException('not in bidding phase'); |
||||||
|
} |
||||||
|
if ($this->bidding['acted'][$player]) { |
||||||
|
throw new InvalidArgumentException('player already bid'); |
||||||
|
} |
||||||
|
$score = \max(0, \min(3, $score)); |
||||||
|
$this->bidding['scores'][$player] = $score; |
||||||
|
$this->bidding['acted'][$player] = true; |
||||||
|
if ($score > $this->bidding['highest']) { |
||||||
|
$this->bidding['highest'] = $score; |
||||||
|
$this->bidding['highestPlayer'] = $player; |
||||||
|
} |
||||||
|
$this->emit('bid', ['player' => $player, 'score' => $score]); |
||||||
|
|
||||||
|
$actedCount = \count(\array_filter($this->bidding['acted'])); |
||||||
|
if ($actedCount < 3 && $score < 3) { |
||||||
|
// 轮到下一位 |
||||||
|
$this->bidding['current'] = ($player + 1) % 3; |
||||||
|
|
||||||
|
return 'ok'; |
||||||
|
} |
||||||
|
|
||||||
|
// 结束叫分 |
||||||
|
if ($this->bidding['highest'] === 0) { |
||||||
|
return 'redeal'; |
||||||
|
} |
||||||
|
$this->startPlaying(); |
||||||
|
|
||||||
|
return 'started'; |
||||||
|
} |
||||||
|
|
||||||
|
private function startPlaying(): void |
||||||
|
{ |
||||||
|
$this->landlord = $this->bidding['highestPlayer']; |
||||||
|
$this->players[$this->landlord]->isLandlord = true; |
||||||
|
// 给地主发底牌 |
||||||
|
foreach ($this->bottom as $c) { |
||||||
|
$this->players[$this->landlord]->hand[] = $c; |
||||||
|
} |
||||||
|
Deck::sort($this->players[$this->landlord]->hand); |
||||||
|
$this->bottomRevealed = true; |
||||||
|
$this->turn = $this->landlord; |
||||||
|
$this->phase = 'playing'; |
||||||
|
$this->emit('gameStart', ['landlord' => $this->landlord]); |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 出牌 |
||||||
|
/** |
||||||
|
* @param list<Card> $cards |
||||||
|
* @param int|null $skillTarget 部分技能需要指定目标 |
||||||
|
*/ |
||||||
|
public function play(int $player, array $cards, ?int $skillTarget = null): void |
||||||
|
{ |
||||||
|
$this->ensureTurn($player); |
||||||
|
if ($this->mods['frozen'][$player] > 0) { |
||||||
|
throw new RuntimeException('玩家被冻结,本回合无法出牌'); |
||||||
|
} |
||||||
|
$combo = Combo::parse($cards); |
||||||
|
if ($combo === null) { |
||||||
|
throw new InvalidArgumentException('无效的牌型'); |
||||||
|
} |
||||||
|
$this->assertCardsInHand($player, $cards); |
||||||
|
|
||||||
|
// 比较:考虑上一手的技能修正 |
||||||
|
if ($this->lastPlay !== null) { |
||||||
|
if (!$this->beatsWithMods($combo, $this->lastPlay)) { |
||||||
|
throw new InvalidArgumentException('压不过上一手'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 白胡子:自身炸弹 +1(arm 标记 bombBonus) |
||||||
|
$bombBonus = 0; |
||||||
|
$hakiActive = false; |
||||||
|
$unblockable = false; |
||||||
|
$ps = $this->players[$player]; |
||||||
|
if ($ps->armed !== null) { |
||||||
|
$skill = Character::byId($ps->characterId)->skill(); |
||||||
|
if ($skill->trigger === 'onPlay' && $this->comboQualifies($combo, $skill)) { |
||||||
|
if ($skill->id === 'shanks_haki') { |
||||||
|
$hakiActive = true; |
||||||
|
} |
||||||
|
if ($skill->id === 'mihawk_unblock') { |
||||||
|
$unblockable = true; |
||||||
|
} |
||||||
|
if ($skill->id === 'garp_shock') { |
||||||
|
// 下家跳过:在 advanceTurn 时处理 |
||||||
|
$this->mods['skipNext'][($player + 1) % 3] = true; |
||||||
|
} |
||||||
|
} |
||||||
|
if ($skill->trigger === 'onBombPlayed' && $combo->isBomb) { |
||||||
|
if ($skill->id === 'whitebeard_quake') { |
||||||
|
$bombBonus = 1; |
||||||
|
} |
||||||
|
} |
||||||
|
// 技能真正生效时才扣费(once/charges/haki),防止无限使用 |
||||||
|
$applied = ($skill->trigger === 'onPlay' && $this->comboQualifies($combo, $skill)) |
||||||
|
|| ($skill->trigger === 'onBombPlayed' && $combo->isBomb); |
||||||
|
if ($applied) { |
||||||
|
$this->consume($player, $skill); |
||||||
|
} |
||||||
|
$ps->armed = null; |
||||||
|
} |
||||||
|
|
||||||
|
// 移除手牌 |
||||||
|
$this->removeCards($player, $cards); |
||||||
|
$this->lastPlay = [ |
||||||
|
'player' => $player, |
||||||
|
'combo' => $combo, |
||||||
|
'hakiActive' => $hakiActive, |
||||||
|
'unblockable' => $unblockable, |
||||||
|
'bombBonus' => $bombBonus, |
||||||
|
]; |
||||||
|
$this->passes = 0; |
||||||
|
$this->emit('play', [ |
||||||
|
'player' => $player, |
||||||
|
'combo' => $combo, |
||||||
|
'hakiActive' => $hakiActive, |
||||||
|
'unblockable' => $unblockable, |
||||||
|
'bombBonus' => $bombBonus, |
||||||
|
]); |
||||||
|
|
||||||
|
// 赤犬反击窗口:若有人 armed 赤犬,自动在 counterBomb 中处理(AI/UI 调用) |
||||||
|
// 此处不自动触发 |
||||||
|
|
||||||
|
if ($this->handCount($player) === 0) { |
||||||
|
$this->finish($player); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$this->advanceTurn(); |
||||||
|
} |
||||||
|
|
||||||
|
public function pass(int $player): void |
||||||
|
{ |
||||||
|
$this->ensureTurn($player); |
||||||
|
if ($this->lastPlay === null) { |
||||||
|
throw new InvalidArgumentException('首出不能过'); |
||||||
|
} |
||||||
|
// 七武海「续命」:技能可改出最小单张(由 armSkill 处理,这里直接过) |
||||||
|
$this->passes++; |
||||||
|
$this->emit('pass', ['player' => $player]); |
||||||
|
$this->advanceTurn(); |
||||||
|
} |
||||||
|
|
||||||
|
/** 赤犬反击:取消对手刚出的炸弹。 */ |
||||||
|
public function counterBomb(int $player): void |
||||||
|
{ |
||||||
|
if ($this->lastPlay === null || !$this->lastPlay['combo']->isBomb) { |
||||||
|
throw new RuntimeException('当前没有可反击的炸弹'); |
||||||
|
} |
||||||
|
$ps = $this->players[$player]; |
||||||
|
if ($ps->characterId === null) { |
||||||
|
throw new RuntimeException('无角色'); |
||||||
|
} |
||||||
|
$skill = Character::byId($ps->characterId)->skill(); |
||||||
|
if ($skill->id !== 'akainu_magma') { |
||||||
|
throw new RuntimeException('该角色无反击炸弹技能'); |
||||||
|
} |
||||||
|
if (!$this->canUse($player, $skill)) { |
||||||
|
throw new RuntimeException('技能不可用'); |
||||||
|
} |
||||||
|
$bomber = $this->lastPlay['player']; |
||||||
|
// 把炸弹牌还给出牌者 |
||||||
|
foreach ($this->lastPlay['combo']->cards as $c) { |
||||||
|
$this->players[$bomber]->hand[] = $c; |
||||||
|
} |
||||||
|
Deck::sort($this->players[$bomber]->hand); |
||||||
|
// 该 trick 作废,反击者获得 lead 权 |
||||||
|
$this->lastPlay = null; |
||||||
|
$this->passes = 0; |
||||||
|
$this->consume($player, $skill); |
||||||
|
$this->mods['bombDisabled'][$bomber] = 2; // 其下个炸弹禁用 |
||||||
|
$this->turn = $player; |
||||||
|
$this->emit('counter', ['player' => $player, 'target' => $bomber]); |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 技能 |
||||||
|
public function canUse(int $player, Skill $skill): bool |
||||||
|
{ |
||||||
|
if ($this->phase !== 'playing') { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($this->mods['petrified'][$player] > 0) { |
||||||
|
return false; // 被石化 |
||||||
|
} |
||||||
|
$ps = $this->players[$player]; |
||||||
|
if ($skill->cost === 'once' && $ps->skill['usedOnce']) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (\str_starts_with($skill->cost, 'charges:')) { |
||||||
|
$n = (int) \substr($skill->cost, 8); |
||||||
|
|
||||||
|
return $ps->skill['chargesLeft'] >= $n; |
||||||
|
} |
||||||
|
if (\str_starts_with($skill->cost, 'haki:')) { |
||||||
|
$n = (int) \substr($skill->cost, 5); |
||||||
|
|
||||||
|
return $ps->skill['haki'] >= $n; |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 激活技能。onTurnStart 立即生效;onPlay/onBombPlayed 预置到下一手。 |
||||||
|
*/ |
||||||
|
public function armSkill(int $player, ?int $target = null): void |
||||||
|
{ |
||||||
|
if ($this->phase !== 'playing') { |
||||||
|
throw new RuntimeException('非出牌阶段'); |
||||||
|
} |
||||||
|
$ps = $this->players[$player]; |
||||||
|
if ($ps->characterId === null) { |
||||||
|
throw new RuntimeException('玩家未分配角色'); |
||||||
|
} |
||||||
|
$skill = Character::byId($ps->characterId)->skill(); |
||||||
|
if (!$this->canUse($player, $skill)) { |
||||||
|
throw new RuntimeException('技能不可用(费用不足或被石化)'); |
||||||
|
} |
||||||
|
|
||||||
|
if ($skill->trigger === 'onTurnStart') { |
||||||
|
Skill::apply($this, $player, $target); |
||||||
|
$this->consume($player, $skill); |
||||||
|
$this->emit('skill', ['player' => $player, 'skill' => $skill->id, 'target' => $target]); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// onPlay / onBombPlayed:预置 |
||||||
|
if ($ps->armed !== null) { |
||||||
|
throw new RuntimeException('已有预置技能'); |
||||||
|
} |
||||||
|
$ps->armed = $skill->id; |
||||||
|
$ps->armedTarget = $target; |
||||||
|
$this->emit('arm', ['player' => $player, 'skill' => $skill->id]); |
||||||
|
} |
||||||
|
|
||||||
|
private function consume(int $player, Skill $skill): void |
||||||
|
{ |
||||||
|
$ps = $this->players[$player]; |
||||||
|
if ($skill->cost === 'once') { |
||||||
|
$ps->skill['usedOnce'] = true; |
||||||
|
} elseif (\str_starts_with($skill->cost, 'charges:')) { |
||||||
|
$n = (int) \substr($skill->cost, 8); |
||||||
|
$ps->skill['chargesLeft'] -= $n; |
||||||
|
} elseif (\str_starts_with($skill->cost, 'haki:')) { |
||||||
|
$n = (int) \substr($skill->cost, 5); |
||||||
|
$ps->skill['haki'] -= $n; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function comboQualifies(Combo $combo, Skill $skill): bool |
||||||
|
{ |
||||||
|
if ($skill->id === 'garp_shock' || $skill->id === 'mihawk_unblock') { |
||||||
|
return $combo->type === 'single' || $combo->type === 'pair'; |
||||||
|
} |
||||||
|
if ($skill->id === 'shanks_haki') { |
||||||
|
return true; // 任意手牌可附霸气 |
||||||
|
} |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 流转 |
||||||
|
private function ensureTurn(int $player): void |
||||||
|
{ |
||||||
|
if ($this->phase !== 'playing') { |
||||||
|
throw new RuntimeException('非出牌阶段'); |
||||||
|
} |
||||||
|
if ($player !== $this->turn) { |
||||||
|
throw new InvalidArgumentException('还没轮到该玩家'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function advanceTurn(): void |
||||||
|
{ |
||||||
|
// trick 结束:连续两人 pass |
||||||
|
if ($this->passes >= 2) { |
||||||
|
$this->lastPlay = null; |
||||||
|
$this->passes = 0; |
||||||
|
// lead 权归上一个出牌者(this->turn 已是最后出牌者,因 pass 不改变 lastPlay 持有者) |
||||||
|
} |
||||||
|
$next = ($this->turn + 1) % 3; |
||||||
|
// 跳过被冻结/被跳过的玩家 |
||||||
|
$guard = 0; |
||||||
|
while ($guard < 3) { |
||||||
|
if ($this->mods['skipNext'][$next]) { |
||||||
|
$this->mods['skipNext'][$next] = false; |
||||||
|
$this->emit('skip', ['player' => $next]); |
||||||
|
$next = ($next + 1) % 3; |
||||||
|
$guard++; |
||||||
|
|
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($this->mods['frozen'][$next] > 0) { |
||||||
|
$this->mods['frozen'][$next]--; |
||||||
|
$this->emit('frozenSkip', ['player' => $next]); |
||||||
|
$next = ($next + 1) % 3; |
||||||
|
$guard++; |
||||||
|
|
||||||
|
continue; |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
// 递减石化/炸弹禁用计数(在对应玩家回合开始时) |
||||||
|
if ($this->mods['petrified'][$next] > 0) { |
||||||
|
$this->mods['petrified'][$next]--; |
||||||
|
} |
||||||
|
if ($this->mods['bombDisabled'][$next] > 0) { |
||||||
|
$this->mods['bombDisabled'][$next]--; |
||||||
|
} |
||||||
|
$this->turn = $next; |
||||||
|
} |
||||||
|
|
||||||
|
private function finish(int $player): void |
||||||
|
{ |
||||||
|
$this->winner = $player; |
||||||
|
$this->winnerSide = $this->side($player); |
||||||
|
$this->phase = 'over'; |
||||||
|
$this->emit('gameOver', ['winner' => $player, 'side' => $this->winnerSide]); |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 比较(含技能修正) |
||||||
|
/** |
||||||
|
* @param array{combo:Combo,hakiActive:bool,unblockable:bool,bombBonus:int} $last |
||||||
|
*/ |
||||||
|
private function beatsWithMods(Combo $a, array $last): bool |
||||||
|
{ |
||||||
|
$b = $last['combo']; |
||||||
|
$bombBonus = $last['bombBonus']; |
||||||
|
// 复制 b 并叠加炸弹加成用于比较 |
||||||
|
if ($bombBonus > 0 && $b->isBomb) { |
||||||
|
$b = new Combo($b->type, $b->rank + $bombBonus, $b->length, $b->cards, true); |
||||||
|
} |
||||||
|
|
||||||
|
// 霸气/不可拦截:非炸不可压 |
||||||
|
if (($last['hakiActive'] || $last['unblockable']) && !$a->isBomb && !$a->isRocket) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
return Combo::beats($a, $b); |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 手牌校验 |
||||||
|
/** |
||||||
|
* @param list<Card> $cards |
||||||
|
*/ |
||||||
|
private function assertCardsInHand(int $player, array $cards): void |
||||||
|
{ |
||||||
|
$handIds = []; |
||||||
|
foreach ($this->players[$player]->hand as $c) { |
||||||
|
$handIds[$c->id()] = true; |
||||||
|
} |
||||||
|
foreach ($cards as $c) { |
||||||
|
if (!isset($handIds[$c->id()])) { |
||||||
|
throw new InvalidArgumentException('手牌中没有该牌: ' . $c->label()); |
||||||
|
} |
||||||
|
$handIds[$c->id()] = false; // 防止同 id 重复(同点不同花色安全) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<Card> $cards |
||||||
|
*/ |
||||||
|
private function removeCards(int $player, array $cards): void |
||||||
|
{ |
||||||
|
$remove = []; |
||||||
|
foreach ($cards as $c) { |
||||||
|
$remove[$c->id()] = ($remove[$c->id()] ?? 0) + 1; |
||||||
|
} |
||||||
|
$kept = []; |
||||||
|
foreach ($this->players[$player]->hand as $c) { |
||||||
|
if (($remove[$c->id()] ?? 0) > 0) { |
||||||
|
$remove[$c->id()]--; |
||||||
|
} else { |
||||||
|
$kept[] = $c; |
||||||
|
} |
||||||
|
} |
||||||
|
$this->players[$player]->hand = $kept; |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 合法着法(AI/提示) |
||||||
|
/** |
||||||
|
* @return list<Combo> |
||||||
|
*/ |
||||||
|
public function legalMoves(int $player): array |
||||||
|
{ |
||||||
|
$hand = $this->players[$player]->hand; |
||||||
|
$all = MoveGenerator::all($hand); |
||||||
|
if ($this->lastPlay === null) { |
||||||
|
return $all; |
||||||
|
} |
||||||
|
|
||||||
|
return \array_values(\array_filter($all, fn (Combo $c) => $this->beatsWithMods($c, $this->lastPlay))); |
||||||
|
} |
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 事件 |
||||||
|
/** |
||||||
|
* @param mixed $payload |
||||||
|
*/ |
||||||
|
private function emit(string $event, $payload): void |
||||||
|
{ |
||||||
|
if (\is_callable($this->onEvent)) { |
||||||
|
($this->onEvent)($event, $payload); |
||||||
|
} |
||||||
|
$this->log[] = \sprintf('[%s] %s', $event, \is_array($payload) ? \json_encode($payload) : (string) $payload); |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,151 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* 从一手牌枚举所有合法牌型(用于 AI 决策与合法性校验)。 |
||||||
|
* 同 (type,rank,length) 的去重,避免手牌多花色导致组合爆炸。 |
||||||
|
*/ |
||||||
|
final class MoveGenerator |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @param list<Card> $hand |
||||||
|
* @return list<Combo> |
||||||
|
*/ |
||||||
|
public static function all(array $hand): array |
||||||
|
{ |
||||||
|
if ($hand === []) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
$byRank = []; |
||||||
|
foreach ($hand as $c) { |
||||||
|
$byRank[$c->rank][] = $c; |
||||||
|
} |
||||||
|
$ranks = \array_keys($byRank); |
||||||
|
\sort($ranks); |
||||||
|
$has = static fn (int $r) => isset($byRank[$r]); |
||||||
|
$cnt = static fn (int $r) => \count($byRank[$r] ?? []); |
||||||
|
|
||||||
|
$out = []; |
||||||
|
$seen = []; |
||||||
|
|
||||||
|
$push = static function (array $cards) use (&$out, &$seen): void { |
||||||
|
$combo = Combo::parse($cards); |
||||||
|
if ($combo === null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$key = $combo->type . ':' . $combo->rank . ':' . $combo->length; |
||||||
|
if (isset($seen[$key])) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$seen[$key] = true; |
||||||
|
$out[] = $combo; |
||||||
|
}; |
||||||
|
|
||||||
|
// 单 / 对 / 三 |
||||||
|
foreach ($ranks as $r) { |
||||||
|
$push([$byRank[$r][0]]); |
||||||
|
if ($cnt($r) >= 2) { |
||||||
|
$push([$byRank[$r][0], $byRank[$r][1]]); |
||||||
|
} |
||||||
|
if ($cnt($r) >= 3) { |
||||||
|
$push([$byRank[$r][0], $byRank[$r][1], $byRank[$r][2]]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 三带一 / 三带二 |
||||||
|
foreach ($ranks as $r) { |
||||||
|
if ($cnt($r) < 3) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$triple = [$byRank[$r][0], $byRank[$r][1], $byRank[$r][2]]; |
||||||
|
foreach ($ranks as $s) { |
||||||
|
if ($s === $r) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ($cnt($s) >= 1) { |
||||||
|
$push(\array_merge($triple, [$byRank[$s][0]])); |
||||||
|
} |
||||||
|
if ($cnt($s) >= 2) { |
||||||
|
$push(\array_merge($triple, [$byRank[$s][0], $byRank[$s][1]])); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 顺子 (5..12 连) |
||||||
|
for ($len = 5; $len <= 12; $len++) { |
||||||
|
for ($start = 3; $start + $len - 1 <= 14; $start++) { |
||||||
|
$ok = true; |
||||||
|
$cards = []; |
||||||
|
for ($k = 0; $k < $len; $k++) { |
||||||
|
$rr = $start + $k; |
||||||
|
if (!$has($rr)) { |
||||||
|
$ok = false; |
||||||
|
break; |
||||||
|
} |
||||||
|
$cards[] = $byRank[$rr][0]; |
||||||
|
} |
||||||
|
if ($ok) { |
||||||
|
$push($cards); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 连对 (3..10 连) |
||||||
|
for ($len = 3; $len <= 10; $len++) { |
||||||
|
for ($start = 3; $start + $len - 1 <= 14; $start++) { |
||||||
|
$ok = true; |
||||||
|
$cards = []; |
||||||
|
for ($k = 0; $k < $len; $k++) { |
||||||
|
$rr = $start + $k; |
||||||
|
if ($cnt($rr) < 2) { |
||||||
|
$ok = false; |
||||||
|
break; |
||||||
|
} |
||||||
|
$cards[] = $byRank[$rr][0]; |
||||||
|
$cards[] = $byRank[$rr][1]; |
||||||
|
} |
||||||
|
if ($ok) { |
||||||
|
$push($cards); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 飞机(纯三连,2..6 连) |
||||||
|
for ($len = 2; $len <= 6; $len++) { |
||||||
|
for ($start = 3; $start + $len - 1 <= 14; $start++) { |
||||||
|
$ok = true; |
||||||
|
$cards = []; |
||||||
|
for ($k = 0; $k < $len; $k++) { |
||||||
|
$rr = $start + $k; |
||||||
|
if ($cnt($rr) < 3) { |
||||||
|
$ok = false; |
||||||
|
break; |
||||||
|
} |
||||||
|
$cards[] = $byRank[$rr][0]; |
||||||
|
$cards[] = $byRank[$rr][1]; |
||||||
|
$cards[] = $byRank[$rr][2]; |
||||||
|
} |
||||||
|
if ($ok) { |
||||||
|
$push($cards); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 炸弹 |
||||||
|
foreach ($ranks as $r) { |
||||||
|
if ($cnt($r) === 4) { |
||||||
|
$push($byRank[$r]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 火箭 |
||||||
|
if ($has(Card::JOKER_SMALL) && $has(Card::JOKER_BIG)) { |
||||||
|
$push([$byRank[Card::JOKER_SMALL][0], $byRank[Card::JOKER_BIG][0]]); |
||||||
|
} |
||||||
|
|
||||||
|
return $out; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* 单名玩家运行时状态。 |
||||||
|
*/ |
||||||
|
final class PlayerState |
||||||
|
{ |
||||||
|
/** @var list<Card> */ |
||||||
|
public array $hand; |
||||||
|
|
||||||
|
public ?string $characterId = null; |
||||||
|
public ?string $faction = null; |
||||||
|
public bool $isLandlord = false; |
||||||
|
|
||||||
|
/** 技能费用状态 */ |
||||||
|
public array $skill = [ |
||||||
|
'chargesLeft' => 0, |
||||||
|
'usedOnce' => false, |
||||||
|
'haki' => 0, |
||||||
|
'cooldown' => 0, |
||||||
|
]; |
||||||
|
|
||||||
|
/** 预置的下一手技能(onPlay/onBombPlayed) */ |
||||||
|
public ?string $armed = null; |
||||||
|
public ?int $armedTarget = null; |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<Card> $hand |
||||||
|
*/ |
||||||
|
public function __construct(array $hand) |
||||||
|
{ |
||||||
|
$this->hand = $hand; |
||||||
|
} |
||||||
|
|
||||||
|
public function initSkill(Skill $skill): void |
||||||
|
{ |
||||||
|
if ($skill->cost === 'once') { |
||||||
|
$this->skill['usedOnce'] = false; |
||||||
|
} elseif (\str_starts_with($skill->cost, 'charges:')) { |
||||||
|
$this->skill['chargesLeft'] = (int) \substr($skill->cost, 8); |
||||||
|
} elseif (\str_starts_with($skill->cost, 'haki:')) { |
||||||
|
$this->skill['haki'] = (int) \substr($skill->cost, 5); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,261 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* Win32 rendering shim. |
||||||
|
* |
||||||
|
* The original GameController was written against libui's DrawContext / |
||||||
|
* Brush / Color / FontDescriptor API. Those symbols are re-declared here, in |
||||||
|
* the SAME namespace, so the controller's drawing code stays byte-for-byte |
||||||
|
* identical while the actual pixels are produced by the Win32 GDI primitives |
||||||
|
* declared in win32.stub.php (implemented in cpp-src/win32.cc). |
||||||
|
* |
||||||
|
* Colour convention: PHP code passes 0xRRGGBB ints. GDI needs 0xBBGGRR, so |
||||||
|
* ddz_rgb() flips the channels on the way down to C++. |
||||||
|
*/ |
||||||
|
|
||||||
|
/* ----------------------------- constants ------------------------------ */ |
||||||
|
|
||||||
|
/** Text alignment — mirrors libui DrawTextAlign. */ |
||||||
|
final class DrawTextAlign |
||||||
|
{ |
||||||
|
public const Left = 0; |
||||||
|
public const Center = 1; |
||||||
|
public const Right = 2; |
||||||
|
} |
||||||
|
|
||||||
|
/** Font weight — mirrors libui TextWeight. */ |
||||||
|
final class TextWeight |
||||||
|
{ |
||||||
|
public const Normal = 400; |
||||||
|
public const Bold = 700; |
||||||
|
} |
||||||
|
|
||||||
|
/* ----------------------------- color types ---------------------------- */ |
||||||
|
|
||||||
|
final class Color |
||||||
|
{ |
||||||
|
public int $hex; |
||||||
|
|
||||||
|
public function __construct(int $hex) |
||||||
|
{ |
||||||
|
$this->hex = $hex & 0xFFFFFF; |
||||||
|
} |
||||||
|
|
||||||
|
public static function rgb(int $hex): self |
||||||
|
{ |
||||||
|
return new self($hex); |
||||||
|
} |
||||||
|
|
||||||
|
/** libui accepts normalised floats here (r,g,b in 0..1). */ |
||||||
|
public static function rgba(float $r, float $g, float $b, float $a): self |
||||||
|
{ |
||||||
|
$rr = (int) ($r * 255); |
||||||
|
$gg = (int) ($g * 255); |
||||||
|
$bb = (int) ($b * 255); |
||||||
|
|
||||||
|
return new self(($rr << 16) | ($gg << 8) | $bb); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class Brush |
||||||
|
{ |
||||||
|
public int $hex; |
||||||
|
|
||||||
|
public function __construct(int $hex) |
||||||
|
{ |
||||||
|
$this->hex = $hex & 0xFFFFFF; |
||||||
|
} |
||||||
|
|
||||||
|
public static function rgb(int $hex): self |
||||||
|
{ |
||||||
|
return new self($hex); |
||||||
|
} |
||||||
|
|
||||||
|
/** Extract the int colour from a Color (or pass-through an int brush). */ |
||||||
|
public static function color($c): int |
||||||
|
{ |
||||||
|
if ($c instanceof Color) { |
||||||
|
return $c->hex; |
||||||
|
} |
||||||
|
if ($c instanceof Brush) { |
||||||
|
return $c->hex; |
||||||
|
} |
||||||
|
|
||||||
|
return (int) $c; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Gradients are approximated by a solid fill in this GDI port. |
||||||
|
* |
||||||
|
* Signature mirrors libui's brushForFill linearGradient: four float |
||||||
|
* coordinates plus a list of [offset, r, g, b, a] stops. We take the |
||||||
|
* colour of the LAST stop (or the deep-navy fallback) as the flat fill. |
||||||
|
* |
||||||
|
* @param array<int, array{0: float, 1: float, 2: float, 3: float, 4?: float}> $stops |
||||||
|
*/ |
||||||
|
public static function linearGradient(float $x0, float $y0, float $x1, float $y1, array $stops): self |
||||||
|
{ |
||||||
|
$hex = 0x08152e; |
||||||
|
if (\count($stops) > 0) { |
||||||
|
$last = $stops[\count($stops) - 1]; |
||||||
|
$r = (int) ($last[1] * 255); |
||||||
|
$g = (int) ($last[2] * 255); |
||||||
|
$b = (int) ($last[3] * 255); |
||||||
|
$hex = ($r << 16) | ($g << 8) | $b; |
||||||
|
} |
||||||
|
|
||||||
|
return new self($hex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class StrokeParams |
||||||
|
{ |
||||||
|
public int $thickness = 1; |
||||||
|
|
||||||
|
public function thickness(int $t): self |
||||||
|
{ |
||||||
|
$this->thickness = $t; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
final class FontDescriptor |
||||||
|
{ |
||||||
|
public string $family; |
||||||
|
public int $size; |
||||||
|
public int $weight; |
||||||
|
|
||||||
|
public function __construct(string $family, int $size, int $weight = TextWeight::Normal) |
||||||
|
{ |
||||||
|
$this->family = $family; |
||||||
|
$this->size = $size; |
||||||
|
$this->weight = $weight; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/* ----------------------------- helpers -------------------------------- */ |
||||||
|
|
||||||
|
/** 0xRRGGBB -> Win32 COLORREF (0xBBGGRR). */ |
||||||
|
function ddz_rgb(int $hex): int |
||||||
|
{ |
||||||
|
$r = ($hex >> 16) & 0xFF; |
||||||
|
$g = ($hex >> 8) & 0xFF; |
||||||
|
$b = $hex & 0xFF; |
||||||
|
|
||||||
|
return ($r) | ($g << 8) | ($b << 16); |
||||||
|
} |
||||||
|
|
||||||
|
/* ------------------- UTF-8 helpers (no mbstring) ---------------------- */ |
||||||
|
|
||||||
|
/** |
||||||
|
* Character (code point) length of a UTF-8 string. |
||||||
|
* mbstring is not linked into the TypePHP runtime, so we count code points |
||||||
|
* with PCRE instead of mb_strlen(). |
||||||
|
*/ |
||||||
|
function ddz_utf8_len(string $s): int |
||||||
|
{ |
||||||
|
if ($s === '') { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
return \preg_match_all('/./us', $s); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Safe UTF-8 substring by code points (no mbstring). |
||||||
|
* Negative $start/$len behave like mb_substr() (offsets from the end). |
||||||
|
*/ |
||||||
|
function ddz_utf8_substr(string $s, int $start, int $len = 0): string |
||||||
|
{ |
||||||
|
if ($s === '') { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
\preg_match_all('/./us', $s, $m); |
||||||
|
$chars = $m[0]; |
||||||
|
$n = \count($chars); |
||||||
|
if ($len === 0) { |
||||||
|
$len = $n; |
||||||
|
} |
||||||
|
if ($start < 0) { |
||||||
|
$start = \max(0, $n + $start); |
||||||
|
} |
||||||
|
if ($len < 0) { |
||||||
|
$len = \max(0, $n - $start + $len); |
||||||
|
} |
||||||
|
$slice = \array_slice($chars, $start, $len); |
||||||
|
|
||||||
|
return \implode('', $slice); |
||||||
|
} |
||||||
|
|
||||||
|
/* --------------------------- draw context ----------------------------- */ |
||||||
|
|
||||||
|
/** |
||||||
|
* Mimics libui's DrawContext but renders through Win32 GDI. The single |
||||||
|
* constructor argument is the memory-DC handle produced by win_begin_paint(). |
||||||
|
*/ |
||||||
|
final class WinDrawContext |
||||||
|
{ |
||||||
|
private int $hdc; |
||||||
|
|
||||||
|
public function __construct(int $hdc) |
||||||
|
{ |
||||||
|
$this->hdc = $hdc; |
||||||
|
} |
||||||
|
|
||||||
|
private function colOf($b): int |
||||||
|
{ |
||||||
|
if ($b instanceof Color || $b instanceof Brush) { |
||||||
|
return $b->hex; |
||||||
|
} |
||||||
|
|
||||||
|
return (int) $b; |
||||||
|
} |
||||||
|
|
||||||
|
public function fillRect(float $x, float $y, float $w, float $h, $brush): void |
||||||
|
{ |
||||||
|
win_fill_rect($this->hdc, (int) $x, (int) $y, (int) $w, (int) $h, ddz_rgb($this->colOf($brush))); |
||||||
|
} |
||||||
|
|
||||||
|
/** Ellipse centred at (cx, cy) — matches libui semantics. */ |
||||||
|
public function fillEllipse(float $cx, float $cy, float $w, float $h, $brush): void |
||||||
|
{ |
||||||
|
win_fill_ellipse($this->hdc, (int) $cx, (int) $cy, (int) $w, (int) $h, ddz_rgb($this->colOf($brush))); |
||||||
|
} |
||||||
|
|
||||||
|
public function fillRoundedRect(float $x, float $y, float $w, float $h, float $r, $brush): void |
||||||
|
{ |
||||||
|
win_fill_rounded_rect($this->hdc, (int) $x, (int) $y, (int) $w, (int) $h, (int) $r, ddz_rgb($this->colOf($brush))); |
||||||
|
} |
||||||
|
|
||||||
|
public function strokeRoundedRect(float $x, float $y, float $w, float $h, float $r, $brush, StrokeParams $stroke): void |
||||||
|
{ |
||||||
|
win_stroke_rounded_rect( |
||||||
|
$this->hdc, |
||||||
|
(int) $x, (int) $y, (int) $w, (int) $h, (int) $r, |
||||||
|
ddz_rgb($this->colOf($brush)), |
||||||
|
(int) $stroke->thickness |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param int $w width of the alignment box (0 = no alignment) |
||||||
|
* @param int $align DrawTextAlign::Left|Center|Right |
||||||
|
*/ |
||||||
|
public function drawString(string $text, FontDescriptor $font, Color $color, float $x, float $y, int $w = 0, int $align = DrawTextAlign::Left): void |
||||||
|
{ |
||||||
|
win_draw_text_ex( |
||||||
|
$this->hdc, |
||||||
|
(int) $x, (int) $y, $text, |
||||||
|
(int) $font->size, |
||||||
|
ddz_rgb($color->hex), |
||||||
|
$font->weight >= TextWeight::Bold ? 1 : 0, |
||||||
|
(int) $w, |
||||||
|
(int) $align |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* 技能定义 + 效果实现。 |
||||||
|
* |
||||||
|
* 效果分类: |
||||||
|
* - onTurnStart 技能:在 armSkill() 时立即由 Skill::apply() 执行(冻结/石化/推牌/偷牌) |
||||||
|
* - onPlay / onBombPlayed 技能:在 Game::play() 中通过预置标记附加到那一手牌(跳过/不可拦截/霸气/炸弹+1) |
||||||
|
* - 赤犬反击:在 Game::counterBomb() 中处理(不属于 arm/apply 路径) |
||||||
|
*/ |
||||||
|
final class Skill |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public string $id, |
||||||
|
public string $name, |
||||||
|
public string $desc, |
||||||
|
public string $trigger, // onTurnStart | onPlay | onBombPlayed |
||||||
|
public string $cost, // once | charges:N | haki:N |
||||||
|
public string $faction, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
/** 应用 onTurnStart 类技能(立即生效)。 */ |
||||||
|
public static function apply(Game $g, int $actor, ?int $target): void |
||||||
|
{ |
||||||
|
$skill = Character::byId($g->players[$actor]->characterId)->skill(); |
||||||
|
$target ??= self::defaultTarget($g, $actor); |
||||||
|
|
||||||
|
switch ($skill->id) { |
||||||
|
case 'aokiji_freeze': |
||||||
|
$g->mods['frozen'][$target] = 1; |
||||||
|
break; |
||||||
|
|
||||||
|
case 'boa_petrify': |
||||||
|
$g->mods['petrified'][$target] = 2; |
||||||
|
break; |
||||||
|
|
||||||
|
case 'kuma_push': |
||||||
|
self::pushCard($g, $actor, $target); |
||||||
|
break; |
||||||
|
|
||||||
|
case 'bigmom_steal': |
||||||
|
self::stealCard($g, $actor, $target); |
||||||
|
break; |
||||||
|
|
||||||
|
default: |
||||||
|
// onPlay / onBombPlayed 类不在 apply 中处理 |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static function defaultTarget(Game $g, int $actor): int |
||||||
|
{ |
||||||
|
// 默认瞄准「手牌最少」的对手(最具威胁) |
||||||
|
$best = null; |
||||||
|
$bestCount = PHP_INT_MAX; |
||||||
|
foreach ([0, 1, 2] as $p) { |
||||||
|
if ($p === $actor) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
$c = $g->handCount($p); |
||||||
|
if ($c < $bestCount) { |
||||||
|
$bestCount = $c; |
||||||
|
$best = $p; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $best ?? (($actor + 1) % 3); |
||||||
|
} |
||||||
|
|
||||||
|
private static function pushCard(Game $g, int $actor, int $target): void |
||||||
|
{ |
||||||
|
$hand = $g->players[$actor]->hand; |
||||||
|
if ($hand === []) { |
||||||
|
return; |
||||||
|
} |
||||||
|
// 推出最小的一张 |
||||||
|
\usort($hand, static fn (Card $a, Card $b) => $a->rank <=> $b->rank); |
||||||
|
$card = \array_shift($hand); |
||||||
|
$g->players[$actor]->hand = $hand; |
||||||
|
$g->players[$target]->hand[] = $card; |
||||||
|
Deck::sort($g->players[$target]->hand); |
||||||
|
} |
||||||
|
|
||||||
|
private static function stealCard(Game $g, int $actor, int $target): void |
||||||
|
{ |
||||||
|
$th = $g->players[$target]->hand; |
||||||
|
if ($th === []) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$idx = \array_rand($th); |
||||||
|
$card = $th[$idx]; |
||||||
|
unset($th[$idx]); |
||||||
|
$th = \array_values($th); |
||||||
|
$g->players[$target]->hand = $th; |
||||||
|
$g->players[$actor]->hand[] = $card; |
||||||
|
Deck::sort($g->players[$actor]->hand); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,107 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Yangweijie\Ui2\Games\OnePieceDoudizhu; |
||||||
|
|
||||||
|
/** |
||||||
|
* Sound-effects manager (TypePHP / Win32 port). |
||||||
|
* |
||||||
|
* The original relied on procedural WAV files loaded through the |
||||||
|
* Yangweijie\Ui2\System\Audio (miniaudio) bridge, which is not available in a |
||||||
|
* statically-compiled TypePHP binary. This port keeps the exact same public |
||||||
|
* API used by GameController (instance / trigger / setEnabled / unload …) but |
||||||
|
* is a safe no-op, so the game logic and event hooks are unchanged. |
||||||
|
* |
||||||
|
* To add real audio later, generate assets/audio/*.wav and call |
||||||
|
* win_message_beep() (or a miniaudio bridge) from trigger(). |
||||||
|
*/ |
||||||
|
final class Sound |
||||||
|
{ |
||||||
|
public const CLICK = 'click'; |
||||||
|
public const DEAL = 'deal'; |
||||||
|
public const PLAY = 'play'; |
||||||
|
public const PASS = 'pass'; |
||||||
|
public const BOMB = 'bomb'; |
||||||
|
public const SKILL = 'skill'; |
||||||
|
public const BID = 'bid'; |
||||||
|
public const WIN = 'win'; |
||||||
|
public const LOSE = 'lose'; |
||||||
|
|
||||||
|
private const DEFAULT_BINDINGS = [ |
||||||
|
'click' => self::CLICK, |
||||||
|
'deal' => self::DEAL, |
||||||
|
'play' => self::PLAY, |
||||||
|
'pass' => self::PASS, |
||||||
|
'bomb' => self::BOMB, |
||||||
|
'skill' => self::SKILL, |
||||||
|
'bid' => self::BID, |
||||||
|
'win' => self::WIN, |
||||||
|
'lose' => self::LOSE, |
||||||
|
]; |
||||||
|
|
||||||
|
private static ?self $instance = null; |
||||||
|
|
||||||
|
private array $bindings; |
||||||
|
private float $volume = 0.8; |
||||||
|
private bool $enabled = true; |
||||||
|
|
||||||
|
public function __construct() |
||||||
|
{ |
||||||
|
$this->bindings = self::DEFAULT_BINDINGS; |
||||||
|
} |
||||||
|
|
||||||
|
public static function instance(): self |
||||||
|
{ |
||||||
|
return self::$instance ??= new self(); |
||||||
|
} |
||||||
|
|
||||||
|
public function setVolume(float $v): self |
||||||
|
{ |
||||||
|
$this->volume = max(0.0, min(1.0, $v)); |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function setEnabled(bool $on): self |
||||||
|
{ |
||||||
|
$this->enabled = $on; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
public function isEnabled(): bool |
||||||
|
{ |
||||||
|
return $this->enabled; |
||||||
|
} |
||||||
|
|
||||||
|
public function bind(string $event, string $sound): self |
||||||
|
{ |
||||||
|
$this->bindings[$event] = $sound; |
||||||
|
|
||||||
|
return $this; |
||||||
|
} |
||||||
|
|
||||||
|
/** Play a sound by name. No-op in this port. */ |
||||||
|
public function play(string $name): void |
||||||
|
{ |
||||||
|
// Intentionally silent: no audio assets in the compiled binary. |
||||||
|
} |
||||||
|
|
||||||
|
/** Fire a named game event; plays the bound sound if any. */ |
||||||
|
public function trigger(string $event): void |
||||||
|
{ |
||||||
|
$sound = $this->bindings[$event] ?? $event; |
||||||
|
$this->play($sound); |
||||||
|
} |
||||||
|
|
||||||
|
public function unload(): void |
||||||
|
{ |
||||||
|
// nothing to release |
||||||
|
} |
||||||
|
|
||||||
|
public function __destruct() |
||||||
|
{ |
||||||
|
$this->unload(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,41 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** |
||||||
|
* Win32 API declarations (stub). |
||||||
|
* |
||||||
|
* C++ (cpp-src/win32.cc) only provides thin wrappers around Win32 APIs and |
||||||
|
* GDI drawing primitives. ALL game logic and rendering live in PHP. |
||||||
|
* |
||||||
|
* These empty declarations tell the TypePHP compiler about the native |
||||||
|
* functions; the real implementations are linked from win32.cc. |
||||||
|
*/ |
||||||
|
|
||||||
|
// ---- Window & message loop ---- |
||||||
|
function win_create_window(string $title, int $width, int $height): int {} |
||||||
|
/** Current client-area size as [width, height]. */ |
||||||
|
function win_get_client_size(int $hWnd): array {} |
||||||
|
function win_show_window(int $hWnd, int $cmdShow): void {} |
||||||
|
function win_quit_requested(): bool {} |
||||||
|
function win_post_quit(int $exitCode): void {} |
||||||
|
/** Returns [type, a, b, c]; empty array when no message pending. */ |
||||||
|
function win_peek_message(): array {} |
||||||
|
function win_get_tick_count(): int {} |
||||||
|
function win_message_box(int $hWnd, string $text, string $caption, int $uType): int {} |
||||||
|
function win_message_beep(int $type): void {} |
||||||
|
|
||||||
|
// ---- Double-buffered frame ---- |
||||||
|
function win_begin_paint(int $hWnd): int {} |
||||||
|
function win_end_paint(int $hWnd, int $hdc): void {} |
||||||
|
|
||||||
|
// ---- GDI primitives ---- |
||||||
|
function win_fill_rect(int $hdc, int $x, int $y, int $w, int $h, int $rgb): void {} |
||||||
|
function win_draw_block(int $hdc, int $x, int $y, int $size, int $rgb): void {} |
||||||
|
function win_draw_line(int $hdc, int $x1, int $y1, int $x2, int $y2, int $rgb): void {} |
||||||
|
/** Ellipse centered at (cx, cy) with width/height. */ |
||||||
|
function win_fill_ellipse(int $hdc, int $cx, int $cy, int $w, int $h, int $rgb): void {} |
||||||
|
function win_fill_rounded_rect(int $hdc, int $x, int $y, int $w, int $h, int $radius, int $rgb): void {} |
||||||
|
function win_stroke_rounded_rect(int $hdc, int $x, int $y, int $w, int $h, int $radius, int $rgb, int $thickness): void {} |
||||||
|
/** Plain ASCII text. */ |
||||||
|
function win_draw_text(int $hdc, int $x, int $y, string $text, int $fontSize, int $rgb, int $bold): void {} |
||||||
|
/** UTF-8 text; align: 0=left, 1=center, 2=right. width=0 means no alignment. */ |
||||||
|
function win_draw_text_ex(int $hdc, int $x, int $y, string $text, int $fontSize, int $rgb, int $bold, int $width, int $align): void {} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
name: onepiece-doudizhu |
||||||
|
version: 1.0.0 |
||||||
|
mode: bin |
||||||
|
sources: |
||||||
|
- main.php |
||||||
|
- ./php-src |
||||||
|
- ./cpp-src |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
use const python\math\pi; |
||||||
|
use function python\platform\python_version; |
||||||
|
|
||||||
|
function main() |
||||||
|
{ |
||||||
|
echo pi, "\n"; |
||||||
|
var_dump(get_class(pi)); |
||||||
|
var_dump(pi->toValue()->toFloat()); |
||||||
|
var_dump(pi->toValue()->toInt()); |
||||||
|
echo python_version(), "\n"; |
||||||
|
} |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** @generated from examples/python/version.py */ |
||||||
|
use python\sys; |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
if (sys\version_info < python\tuple([3, 8])) { |
||||||
|
echo '此脚本需要 Python 3.8 或更高版本', "\n"; |
||||||
|
exit(1); |
||||||
|
} else { |
||||||
|
echo '当前 Python 版本: ' . |
||||||
|
sys\version_info->major->toString() . '.' . |
||||||
|
sys\version_info->minor->toString() . '.' . |
||||||
|
sys\version_info->micro->toString(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import sys |
||||||
|
|
||||||
|
if sys.version_info < (3, 8): |
||||||
|
print("此脚本需要 Python 3.8 或更高版本") |
||||||
|
sys.exit(1) |
||||||
|
else: |
||||||
|
print(f"当前 Python 版本: {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}") |
||||||
|
|
||||||
@ -0,0 +1,5 @@ |
|||||||
|
/build/ |
||||||
|
/component/ |
||||||
|
/dist/ |
||||||
|
/generated/ |
||||||
|
/node_modules/ |
||||||
@ -0,0 +1,73 @@ |
|||||||
|
# TypePHP WASI Browser Lab |
||||||
|
|
||||||
|
这是一个由 `project.yml` 构建的完整 TypePHP/WASI 0.2 浏览器应用。PHP 代码编译成一个自包含的 Component,Jco 将同一个 Component 转译为浏览器 ESM,页面通过 module Worker 加载它。 |
||||||
|
|
||||||
|
Demo 展示以下已支持能力: |
||||||
|
|
||||||
|
- 命令行参数、环境变量与标准输入/输出 |
||||||
|
- WASI wall clock 和 PHP `time()` / `date()` |
||||||
|
- 安全随机数 `random_int()` / `random_bytes()` |
||||||
|
- 内存文件系统,以及可选的 OPFS 快照持久化 |
||||||
|
- 通过同步 `file_get_contents()` 发起 HTTP/HTTPS GET;浏览器等待期间由 JSPI 挂起 Wasm 调用栈 |
||||||
|
- PHP 8.5 runtime 信息 |
||||||
|
- 由 `get_loaded_extensions()` 动态读取的 PHP/WASI 内置扩展列表;点击扩展后,JavaScript 调用 `#[WasmExport]` 导出的函数读取版本、函数、类、常量和 INI 配置 |
||||||
|
- TypePHP 语言级 BigInt、Decimal、BigFloat 高精度计算 |
||||||
|
|
||||||
|
原始 socket、进程、shell、信号、Fiber 和 Generator 明确不支持。 |
||||||
|
|
||||||
|
浏览器构建要求支持 `WebAssembly.Suspending` 和 `WebAssembly.promising` |
||||||
|
的 JSPI 实现。HTTP 请求仍受浏览器 CORS、CSP 和 Mixed Content 策略约束。 |
||||||
|
`file_get_contents()` 当前支持 GET、`http.timeout` 和 |
||||||
|
`http.ignore_errors`;不提供 Curl API,也不会退化为忙等待。 |
||||||
|
|
||||||
|
## 构建 |
||||||
|
|
||||||
|
先确保 WASI SDK 和 Wasmtime 已加入 `PATH`,然后在本目录执行: |
||||||
|
|
||||||
|
```bash |
||||||
|
npm ci |
||||||
|
npm run wasm |
||||||
|
``` |
||||||
|
|
||||||
|
等价的仓库根目录命令是: |
||||||
|
|
||||||
|
```bash |
||||||
|
php bin/tpc.php examples/wasm-hello/project.yml |
||||||
|
``` |
||||||
|
|
||||||
|
`project.yml` 控制全部项目路径: |
||||||
|
|
||||||
|
- `sources: src`:TypePHP 源码 |
||||||
|
- `mode: library`:生成可由 JavaScript 多次调用的 Component,而不是运行一次即退出的命令 |
||||||
|
- `build-dir: build`:生成的 C++ 与目标文件 |
||||||
|
- `output: component/wasm-hello.wasm`:WASI 0.2 Component |
||||||
|
- `wasm: browser`:显式生成浏览器模块;简单项目使用 `wasm: component` 只生成 Component |
||||||
|
- 未配置 `target-platform` 时,WASM 项目默认使用 `wasm32-wasip2` |
||||||
|
- `wasm-browser-dir: generated`:Jco 浏览器模块 |
||||||
|
- `wasm-package` 和 `wasm-world`:定义导出接口的稳定 WIT 名称 |
||||||
|
|
||||||
|
Jco 是本项目的开发依赖。先执行 `npm ci`,之后通过 `npm run wasm` 构建时,npm 会自动把本地 `node_modules/.bin/jco` 加入 `PATH`。 |
||||||
|
|
||||||
|
若只需要供 Wasmtime 使用的 Component,可以绕过 Jco: |
||||||
|
|
||||||
|
```bash |
||||||
|
php ../../bin/tpc.php project.yml --wasm=component |
||||||
|
``` |
||||||
|
|
||||||
|
## 浏览器运行 |
||||||
|
|
||||||
|
```bash |
||||||
|
npm run dev |
||||||
|
``` |
||||||
|
|
||||||
|
打开终端显示的本地地址。可以修改参数、环境变量和 stdin 后重复运行;勾选 OPFS 后,PHP 写入虚拟文件系统的运行次数会跨页面刷新保存。点击任意 PHP 扩展名称,页面会向 Worker 发送请求,Worker 调用 Wasm `runtime.getExtensionInfo()` 导出函数,最后由 JavaScript 解析返回的 JSON 并渲染扩展详情。 |
||||||
|
|
||||||
|
生产构建: |
||||||
|
|
||||||
|
```bash |
||||||
|
npm run build |
||||||
|
``` |
||||||
|
|
||||||
|
`src/` 是 TypePHP 应用,`typephp-worker.mjs` 是浏览器 WASI host,`main.js` 和 `style.css` 负责交互界面。`build/`、`component/`、`dist/`、`generated/` 均为可重新生成的输出。 |
||||||
|
|
||||||
|
WASI 运行库、PHPX 和生成的 C++ 均使用 `-O2` 编译。最终链接会移除调试与符号段,以减少浏览器下载、解析和编译 Wasm 的开销;构建过程不会自动调用系统中的 `wasm-opt`,避免旧版 Binaryen 与 WASI SDK 生成的 Wasm 异常指令不兼容。 |
||||||
@ -0,0 +1,134 @@ |
|||||||
|
<!doctype html> |
||||||
|
<html lang="zh-CN"> |
||||||
|
<head> |
||||||
|
<meta charset="UTF-8"> |
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
||||||
|
<meta name="theme-color" content="#07111f"> |
||||||
|
<title>TypePHP · WASI 0.2 Browser Lab</title> |
||||||
|
<link rel="stylesheet" href="/style.css"> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div class="ambient ambient-one"></div> |
||||||
|
<div class="ambient ambient-two"></div> |
||||||
|
<main class="shell"> |
||||||
|
<header class="hero"> |
||||||
|
<div> |
||||||
|
<p class="eyebrow"><span class="pulse"></span> WASI 0.2 · Preview 2</p> |
||||||
|
<h1>TypePHP<br><span>Browser Lab</span></h1> |
||||||
|
<p class="lede">同一个由 PHP、PHPX 与 TypePHP 静态链接的 Component,在浏览器 Worker 中直接执行。</p> |
||||||
|
</div> |
||||||
|
<div class="runtime-orbit" aria-hidden="true"> |
||||||
|
<div class="orbit-ring"></div> |
||||||
|
<div class="runtime-core">PHP<br><strong>AOT</strong></div> |
||||||
|
<span class="satellite one">WASI</span> |
||||||
|
<span class="satellite two">Wasm</span> |
||||||
|
<span class="satellite three">C++17</span> |
||||||
|
</div> |
||||||
|
</header> |
||||||
|
|
||||||
|
<section class="control-panel panel"> |
||||||
|
<div class="panel-heading"> |
||||||
|
<div> |
||||||
|
<p class="section-label">Launch configuration</p> |
||||||
|
<h2>配置本次运行</h2> |
||||||
|
</div> |
||||||
|
<div id="status" class="status idle"><span></span>Ready</div> |
||||||
|
</div> |
||||||
|
<div class="form-grid"> |
||||||
|
<label> |
||||||
|
<span>命令行参数</span> |
||||||
|
<input id="args" value="Ada 42 --browser" autocomplete="off"> |
||||||
|
<small>空格分隔,支持单引号和双引号</small> |
||||||
|
</label> |
||||||
|
<label> |
||||||
|
<span>环境变量</span> |
||||||
|
<input id="env" value="DEMO_GREETING=Hello from Chrome" autocomplete="off"> |
||||||
|
<small>每行一个 KEY=VALUE</small> |
||||||
|
</label> |
||||||
|
<label class="wide"> |
||||||
|
<span>标准输入</span> |
||||||
|
<textarea id="stdin" rows="2">A message sent through WASI stdin.</textarea> |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<div class="actions"> |
||||||
|
<label class="switch-row"> |
||||||
|
<input id="persistent" type="checkbox" checked> |
||||||
|
<span class="switch"><i></i></span> |
||||||
|
<span><strong>OPFS 持久化</strong><small>刷新页面后保留虚拟文件系统</small></span> |
||||||
|
</label> |
||||||
|
<div class="button-row"> |
||||||
|
<button id="reset" class="button secondary" type="button">清空存储</button> |
||||||
|
<button id="run" class="button primary" type="button"><span>运行 TypePHP</span><b>→</b></button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section class="results"> |
||||||
|
<article class="feature-card accent-cyan"> |
||||||
|
<div class="icon">⌁</div><p>Runtime</p><h3 id="runtime-value">等待运行</h3><small id="platform-value">—</small> |
||||||
|
</article> |
||||||
|
<article class="feature-card accent-violet"> |
||||||
|
<div class="icon">◷</div><p>Clock</p><h3 id="clock-value">—</h3><small>WASI wall clock</small> |
||||||
|
</article> |
||||||
|
<article class="feature-card accent-amber"> |
||||||
|
<div class="icon">✦</div><p>Secure random</p><h3 id="random-value">—</h3><small id="token-value">WASI random</small> |
||||||
|
</article> |
||||||
|
<article class="feature-card accent-green"> |
||||||
|
<div class="icon">▱</div><p>Filesystem</p><h3 id="filesystem-value">—</h3><small id="files-value">Memory FS + OPFS snapshot</small> |
||||||
|
</article> |
||||||
|
<article class="feature-card accent-cyan"> |
||||||
|
<div class="icon">⇄</div><p>HTTP fetch</p><h3 id="http-value">—</h3><small id="http-detail">file_get_contents() + WASI HTTP</small> |
||||||
|
</article> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section class="detail-grid"> |
||||||
|
<article class="panel data-panel"> |
||||||
|
<p class="section-label">Guest inputs</p> |
||||||
|
<h2>参数、环境与标准流</h2> |
||||||
|
<dl> |
||||||
|
<div><dt>argv</dt><dd id="argv-value">—</dd></div> |
||||||
|
<div><dt>env</dt><dd id="env-value">—</dd></div> |
||||||
|
<div><dt>stdin</dt><dd id="stdin-value">—</dd></div> |
||||||
|
</dl> |
||||||
|
</article> |
||||||
|
<article class="panel precision-panel"> |
||||||
|
<p class="section-label">Language-level numeric types</p> |
||||||
|
<h2>高精度计算</h2> |
||||||
|
<dl> |
||||||
|
<div><dt>BigInt</dt><dd id="bigint-value">—</dd></div> |
||||||
|
<div><dt>Decimal</dt><dd id="decimal-value">—</dd></div> |
||||||
|
<div><dt>BigFloat</dt><dd id="bigfloat-value">—</dd></div> |
||||||
|
</dl> |
||||||
|
</article> |
||||||
|
</section> |
||||||
|
|
||||||
|
<section class="panel extensions-panel"> |
||||||
|
<div> |
||||||
|
<p class="section-label">PHP runtime</p> |
||||||
|
<h2>静态编译的 PHP 扩展</h2> |
||||||
|
</div> |
||||||
|
<strong id="extension-count">等待运行</strong> |
||||||
|
<div id="extension-list" class="extension-list" aria-live="polite"> |
||||||
|
<span>由 get_loaded_extensions() 动态读取</span> |
||||||
|
</div> |
||||||
|
<article id="extension-detail" class="extension-detail" aria-live="polite"> |
||||||
|
<div class="extension-empty"> |
||||||
|
<strong>选择一个扩展</strong> |
||||||
|
<span>点击上方扩展名称,由 JavaScript 调用 Wasm 导出函数读取详情。</span> |
||||||
|
</div> |
||||||
|
</article> |
||||||
|
</section> |
||||||
|
|
||||||
|
<details class="console panel"> |
||||||
|
<summary><span>Raw component output</span><kbd>stdout / stderr</kbd></summary> |
||||||
|
<pre id="output">尚未运行。</pre> |
||||||
|
</details> |
||||||
|
|
||||||
|
<footer> |
||||||
|
<span>TypePHP → C++17 → WASI 0.2 Component</span> |
||||||
|
<span>运行于独立 ES module Worker · HTTP 通过 JSPI 挂起等待</span> |
||||||
|
</footer> |
||||||
|
</main> |
||||||
|
<script type="module" src="/main.js"></script> |
||||||
|
</body> |
||||||
|
</html> |
||||||
@ -0,0 +1,231 @@ |
|||||||
|
const storageName = 'typephp-wasi-demo-filesystem.json'; |
||||||
|
const elements = Object.fromEntries([ |
||||||
|
'args', 'env', 'stdin', 'persistent', 'run', 'reset', 'status', 'output', |
||||||
|
'runtime-value', 'platform-value', 'clock-value', 'random-value', 'token-value', |
||||||
|
'filesystem-value', 'files-value', 'argv-value', 'env-value', 'stdin-value', |
||||||
|
'http-value', 'http-detail', 'bigint-value', 'decimal-value', 'bigfloat-value', |
||||||
|
'extension-count', 'extension-list', |
||||||
|
].map((id) => [id, document.getElementById(id)])); |
||||||
|
|
||||||
|
let worker = null; |
||||||
|
let selectedExtension = ''; |
||||||
|
|
||||||
|
function parseArguments(source) { |
||||||
|
const args = []; |
||||||
|
const pattern = /"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'|([^\s]+)/g; |
||||||
|
for (const match of source.matchAll(pattern)) { |
||||||
|
args.push((match[1] ?? match[2] ?? match[3]).replace(/\\([\\"'])/g, '$1')); |
||||||
|
} |
||||||
|
return args; |
||||||
|
} |
||||||
|
|
||||||
|
function parseEnvironment(source) { |
||||||
|
return Object.fromEntries(source.split(/\r?\n/).flatMap((line) => { |
||||||
|
const separator = line.indexOf('='); |
||||||
|
return separator > 0 ? [[line.slice(0, separator).trim(), line.slice(separator + 1)]] : []; |
||||||
|
})); |
||||||
|
} |
||||||
|
|
||||||
|
function setStatus(kind, label) { |
||||||
|
elements.status.className = `status ${kind}`; |
||||||
|
elements.status.lastChild.textContent = label; |
||||||
|
} |
||||||
|
|
||||||
|
function value(id, content) { |
||||||
|
elements[id].textContent = content === '' ? '(空)' : String(content ?? '—'); |
||||||
|
} |
||||||
|
|
||||||
|
function renderReport(report) { |
||||||
|
value('runtime-value', `PHP ${report.runtime.php}`); |
||||||
|
value('platform-value', report.runtime.platform); |
||||||
|
value('extension-count', `${report.runtime.extensions.length} 个内置扩展`); |
||||||
|
elements['extension-list'].replaceChildren(...report.runtime.extensions.map((extension) => { |
||||||
|
const button = document.createElement('button'); |
||||||
|
button.type = 'button'; |
||||||
|
button.className = 'extension-button'; |
||||||
|
button.dataset.extension = extension; |
||||||
|
button.textContent = extension; |
||||||
|
button.title = `查看 ${extension} 扩展信息`; |
||||||
|
return button; |
||||||
|
})); |
||||||
|
value('clock-value', report.clock.iso8601); |
||||||
|
value('random-value', report.random.integer); |
||||||
|
value('token-value', report.random.token); |
||||||
|
value('filesystem-value', `第 ${report.filesystem.run} 次运行`); |
||||||
|
value('files-value', report.filesystem.files.join(' · ')); |
||||||
|
value('http-value', report.http.ok ? `${report.http.bytes} bytes` : '请求失败'); |
||||||
|
value('http-detail', report.http.preview); |
||||||
|
value('argv-value', report.input.argv.join(' ')); |
||||||
|
value('env-value', report.input.greeting); |
||||||
|
value('stdin-value', report.input.stdin); |
||||||
|
value('bigint-value', report.precision.bigint); |
||||||
|
value('decimal-value', report.precision.decimal); |
||||||
|
value('bigfloat-value', report.precision.bigfloat); |
||||||
|
} |
||||||
|
|
||||||
|
function appendCodeList(container, values, emptyLabel = '无') { |
||||||
|
if (!Array.isArray(values) || values.length === 0) { |
||||||
|
const empty = document.createElement('span'); |
||||||
|
empty.textContent = emptyLabel; |
||||||
|
container.append(empty); |
||||||
|
return; |
||||||
|
} |
||||||
|
for (const item of values) { |
||||||
|
const code = document.createElement('code'); |
||||||
|
code.textContent = String(item); |
||||||
|
code.title = String(item); |
||||||
|
container.append(code); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function extensionGroup(title, content) { |
||||||
|
const group = document.createElement('section'); |
||||||
|
group.className = 'extension-group'; |
||||||
|
const heading = document.createElement('h4'); |
||||||
|
heading.textContent = title; |
||||||
|
const items = document.createElement('div'); |
||||||
|
items.className = 'extension-items'; |
||||||
|
content(items); |
||||||
|
group.append(heading, items); |
||||||
|
return group; |
||||||
|
} |
||||||
|
|
||||||
|
function keyValueList(container, values) { |
||||||
|
const entries = values && typeof values === 'object' ? Object.entries(values) : []; |
||||||
|
if (entries.length === 0) { |
||||||
|
appendCodeList(container, []); |
||||||
|
return; |
||||||
|
} |
||||||
|
const list = document.createElement('dl'); |
||||||
|
for (const [key, item] of entries) { |
||||||
|
const row = document.createElement('div'); |
||||||
|
const term = document.createElement('dt'); |
||||||
|
const value = document.createElement('dd'); |
||||||
|
term.textContent = key; |
||||||
|
value.textContent = typeof item === 'string' ? item : JSON.stringify(item); |
||||||
|
row.append(term, value); |
||||||
|
list.append(row); |
||||||
|
} |
||||||
|
container.append(list); |
||||||
|
} |
||||||
|
|
||||||
|
function renderExtensionInfo(info) { |
||||||
|
const heading = document.createElement('div'); |
||||||
|
heading.className = 'extension-detail-header'; |
||||||
|
const identity = document.createElement('div'); |
||||||
|
const name = document.createElement('h3'); |
||||||
|
name.textContent = info.name; |
||||||
|
const version = document.createElement('p'); |
||||||
|
version.textContent = `version ${info.version}`; |
||||||
|
identity.append(name, version); |
||||||
|
const flags = document.createElement('div'); |
||||||
|
flags.className = 'extension-flags'; |
||||||
|
for (const label of [info.persistent ? 'persistent' : 'non-persistent', info.temporary ? 'temporary' : 'built-in']) { |
||||||
|
const flag = document.createElement('span'); |
||||||
|
flag.textContent = label; |
||||||
|
flags.append(flag); |
||||||
|
} |
||||||
|
heading.append(identity, flags); |
||||||
|
|
||||||
|
const groups = document.createElement('div'); |
||||||
|
groups.className = 'extension-groups'; |
||||||
|
groups.append( |
||||||
|
extensionGroup(`Functions · ${info.functions.length}`, (node) => appendCodeList(node, info.functions)), |
||||||
|
extensionGroup(`Classes · ${info.classes.length}`, (node) => appendCodeList(node, info.classes)), |
||||||
|
extensionGroup(`Constants · ${info.constants.length}`, (node) => appendCodeList(node, info.constants)), |
||||||
|
extensionGroup('INI entries', (node) => keyValueList(node, info.iniEntries)), |
||||||
|
extensionGroup('Dependencies', (node) => keyValueList(node, info.dependencies)), |
||||||
|
); |
||||||
|
document.getElementById('extension-detail').replaceChildren(heading, groups); |
||||||
|
} |
||||||
|
|
||||||
|
function loadExtensionInfo(extension) { |
||||||
|
if (!worker) return; |
||||||
|
selectedExtension = extension; |
||||||
|
for (const button of elements['extension-list'].querySelectorAll('.extension-button')) { |
||||||
|
button.classList.toggle('active', button.dataset.extension === extension); |
||||||
|
} |
||||||
|
document.getElementById('extension-detail').innerHTML = '<span class="extension-loading">正在调用 Wasm 导出函数…</span>'; |
||||||
|
worker.postMessage({ type: 'extension-info', extension }); |
||||||
|
} |
||||||
|
|
||||||
|
function run() { |
||||||
|
worker?.terminate(); |
||||||
|
worker = new Worker(new URL('./typephp-worker.mjs', import.meta.url), { type: 'module' }); |
||||||
|
let stdout = ''; |
||||||
|
let stderr = ''; |
||||||
|
selectedExtension = ''; |
||||||
|
|
||||||
|
elements.run.disabled = true; |
||||||
|
elements.output.textContent = '正在实例化 WASI 0.2 Component…'; |
||||||
|
setStatus('running', 'Running'); |
||||||
|
|
||||||
|
worker.onmessage = ({ data }) => { |
||||||
|
if (data.type === 'stdout') { |
||||||
|
stdout += data.data; |
||||||
|
} else if (data.type === 'stderr') { |
||||||
|
stderr += data.data; |
||||||
|
} else if (data.type === 'error') { |
||||||
|
stderr += `${data.error}\n`; |
||||||
|
elements.run.disabled = false; |
||||||
|
setStatus('error', 'Wasm error'); |
||||||
|
elements.output.textContent = [stdout, stderr].filter(Boolean).join('\n--- stderr ---\n'); |
||||||
|
} else if (data.type === 'report') { |
||||||
|
elements.run.disabled = false; |
||||||
|
elements.output.textContent = [data.json, stdout, stderr].filter(Boolean).join('\n--- component output ---\n'); |
||||||
|
try { |
||||||
|
renderReport(JSON.parse(data.json)); |
||||||
|
setStatus('success', 'Ready for JS calls'); |
||||||
|
} catch (error) { |
||||||
|
setStatus('error', 'Invalid export result'); |
||||||
|
elements.output.textContent += `\n\nUI parse error: ${error.message}`; |
||||||
|
} |
||||||
|
} else if (data.type === 'extension-info') { |
||||||
|
if (data.extension === selectedExtension) { |
||||||
|
try { |
||||||
|
renderExtensionInfo(JSON.parse(data.json)); |
||||||
|
} catch (error) { |
||||||
|
document.getElementById('extension-detail').textContent = `无法解析扩展信息:${error.message}`; |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (data.type === 'extension-error' && data.extension === selectedExtension) { |
||||||
|
document.getElementById('extension-detail').textContent = data.error; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
worker.onerror = (event) => { |
||||||
|
elements.run.disabled = false; |
||||||
|
setStatus('error', 'Worker error'); |
||||||
|
elements.output.textContent = event.message; |
||||||
|
}; |
||||||
|
|
||||||
|
worker.postMessage({ |
||||||
|
type: 'run', |
||||||
|
args: parseArguments(elements.args.value), |
||||||
|
env: parseEnvironment(elements.env.value), |
||||||
|
stdin: elements.stdin.value, |
||||||
|
persistent: elements.persistent.checked, |
||||||
|
storageName, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async function resetStorage() { |
||||||
|
if (!navigator.storage?.getDirectory) { |
||||||
|
setStatus('error', 'OPFS unavailable'); |
||||||
|
return; |
||||||
|
} |
||||||
|
const root = await navigator.storage.getDirectory(); |
||||||
|
await root.removeEntry(storageName).catch((error) => { |
||||||
|
if (error.name !== 'NotFoundError') throw error; |
||||||
|
}); |
||||||
|
setStatus('idle', 'Storage cleared'); |
||||||
|
value('filesystem-value', '等待重新运行'); |
||||||
|
} |
||||||
|
|
||||||
|
elements.run.addEventListener('click', run); |
||||||
|
elements.reset.addEventListener('click', () => resetStorage().catch((error) => setStatus('error', error.message))); |
||||||
|
elements['extension-list'].addEventListener('click', (event) => { |
||||||
|
const button = event.target.closest('.extension-button'); |
||||||
|
if (button) loadExtensionInfo(button.dataset.extension); |
||||||
|
}); |
||||||
|
run(); |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@ |
|||||||
|
{ |
||||||
|
"name": "typephp-wasm-hello", |
||||||
|
"version": "0.1.0", |
||||||
|
"private": true, |
||||||
|
"type": "module", |
||||||
|
"scripts": { |
||||||
|
"wasm": "php ../../bin/tpc.php project.yml", |
||||||
|
"dev": "vite", |
||||||
|
"build": "vite build" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@bytecodealliance/preview2-shim": "0.20.1" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@bytecodealliance/jco": "^1.27.0", |
||||||
|
"vite": "^7.1.0" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
name: wasm-hello |
||||||
|
mode: library |
||||||
|
build-dir: build |
||||||
|
output: component/wasm-hello.wasm |
||||||
|
sources: |
||||||
|
- src |
||||||
|
wasm: browser |
||||||
|
wasm-browser-dir: generated |
||||||
|
wasm-package: typephp:wasm-hello@1.0.0 |
||||||
|
wasm-world: wasm-hello |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"message": "Hello from browser fetch via PHP file_get_contents()", |
||||||
|
"runtime": "WASI HTTP 0.2 + JSPI" |
||||||
|
} |
||||||
@ -0,0 +1,145 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
use native_types; |
||||||
|
|
||||||
|
final class WasiDemo |
||||||
|
{ |
||||||
|
public static function report(array $arguments, string $greeting, string $stdin): array |
||||||
|
{ |
||||||
|
if ($greeting === '') { |
||||||
|
$greeting = 'Hello from the WASI environment'; |
||||||
|
} |
||||||
|
|
||||||
|
$argv = array_merge(['typephp.wasm'], $arguments); |
||||||
|
|
||||||
|
return [ |
||||||
|
'runtime' => [ |
||||||
|
'php' => phpversion(), |
||||||
|
'platform' => php_uname(), |
||||||
|
'integerBits' => PHP_INT_SIZE * 8, |
||||||
|
'extensions' => get_loaded_extensions(), |
||||||
|
], |
||||||
|
'clock' => [ |
||||||
|
'timestamp' => time(), |
||||||
|
'iso8601' => date('Y-m-d H:i:s T'), |
||||||
|
'microtime' => microtime(true), |
||||||
|
], |
||||||
|
'random' => [ |
||||||
|
'integer' => random_int(100000, 999999), |
||||||
|
'token' => bin2hex(random_bytes(8)), |
||||||
|
], |
||||||
|
'input' => [ |
||||||
|
'argc' => count($argv), |
||||||
|
'argv' => $argv, |
||||||
|
'greeting' => $greeting, |
||||||
|
'stdin' => trim($stdin), |
||||||
|
], |
||||||
|
'filesystem' => self::filesystemReport(), |
||||||
|
'http' => self::httpReport(), |
||||||
|
'precision' => self::precisionReport(), |
||||||
|
'capabilities' => [ |
||||||
|
'supported' => ['arguments', 'environment', 'stdin/stdout/stderr', 'clock', 'random', 'filesystem', 'HTTP GET'], |
||||||
|
'disabled' => ['raw sockets', 'process', 'signals', 'shell', 'Fiber', 'Generator'], |
||||||
|
], |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function extensionInfo(string $name): array |
||||||
|
{ |
||||||
|
if (!extension_loaded($name)) { |
||||||
|
throw new InvalidArgumentException("PHP extension '{$name}' is not loaded"); |
||||||
|
} |
||||||
|
|
||||||
|
$extension = new ReflectionExtension($name); |
||||||
|
$functions = get_extension_funcs($name); |
||||||
|
if ($functions === false) { |
||||||
|
$functions = []; |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'name' => $extension->getName(), |
||||||
|
'version' => $extension->getVersion() ?: 'built-in', |
||||||
|
'persistent' => $extension->isPersistent(), |
||||||
|
'temporary' => $extension->isTemporary(), |
||||||
|
'dependencies' => $extension->getDependencies(), |
||||||
|
'iniEntries' => $extension->getINIEntries(), |
||||||
|
'constants' => array_keys($extension->getConstants()), |
||||||
|
'functions' => array_values($functions), |
||||||
|
'classes' => $extension->getClassNames(), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
private static function httpReport(): array |
||||||
|
{ |
||||||
|
$url = getenv('TYPEPHP_FETCH_URL'); |
||||||
|
if ($url === false || $url === '') { |
||||||
|
return ['ok' => false, 'url' => '', 'bytes' => 0, 'preview' => 'No URL configured']; |
||||||
|
} |
||||||
|
|
||||||
|
$body = file_get_contents($url); |
||||||
|
if ($body === false) { |
||||||
|
return ['ok' => false, 'url' => $url, 'bytes' => 0, 'preview' => 'Request failed']; |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'ok' => true, |
||||||
|
'url' => $url, |
||||||
|
'bytes' => strlen($body), |
||||||
|
'preview' => trim(substr($body, 0, 80)), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
private static function filesystemReport(): array |
||||||
|
{ |
||||||
|
$directory = '/workspace'; |
||||||
|
if (!is_dir($directory)) { |
||||||
|
mkdir($directory); |
||||||
|
} |
||||||
|
|
||||||
|
$counterFile = $directory . '/run-count.txt'; |
||||||
|
$counter = 0; |
||||||
|
if (file_exists($counterFile)) { |
||||||
|
$counter = (int) trim((string) file_get_contents($counterFile)); |
||||||
|
} |
||||||
|
$counter++; |
||||||
|
file_put_contents($counterFile, (string) $counter); |
||||||
|
|
||||||
|
$messageFile = $directory . '/hello.txt'; |
||||||
|
$message = 'TypePHP wrote this file during browser run #' . $counter; |
||||||
|
file_put_contents($messageFile, $message); |
||||||
|
|
||||||
|
$files = scandir($directory); |
||||||
|
if ($files === false) { |
||||||
|
$files = []; |
||||||
|
} |
||||||
|
$visibleFiles = array_values(array_diff($files, ['.', '..'])); |
||||||
|
|
||||||
|
return [ |
||||||
|
'run' => $counter, |
||||||
|
'readback' => (string) file_get_contents($messageFile), |
||||||
|
'files' => $visibleFiles, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
private static function precisionReport(): array |
||||||
|
{ |
||||||
|
$big = std::bigInt('123456789012345678901234567890'); |
||||||
|
$bigResult = ($big * std::bigInt(1000000) + std::bigInt(42))->toString(); |
||||||
|
|
||||||
|
$price = std::decimal('199.95'); |
||||||
|
$taxRate = std::decimal('0.0825'); |
||||||
|
$decimalResult = ($price * (std::decimal(1) + $taxRate))->toString(); |
||||||
|
|
||||||
|
$pi = std::bigFloat('3.141592653589793238462643383279502884197'); |
||||||
|
$radius = std::bigFloat(12); |
||||||
|
$bigFloatResult = ($pi * $radius * $radius)->toString(); |
||||||
|
|
||||||
|
return [ |
||||||
|
'bigint' => $bigResult, |
||||||
|
'decimal' => $decimalResult, |
||||||
|
'bigfloat' => $bigFloatResult, |
||||||
|
]; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,23 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
#[WasmExport(name: 'get-demo-report')] |
||||||
|
function getDemoReport(string $argumentsJson, string $greeting, string $stdin): string |
||||||
|
{ |
||||||
|
$arguments = json_decode($argumentsJson, true, flags: JSON_THROW_ON_ERROR); |
||||||
|
$report = WasiDemo::report($arguments, $greeting, $stdin); |
||||||
|
return json_encode( |
||||||
|
$report, |
||||||
|
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
#[WasmExport(name: 'get-extension-info')] |
||||||
|
function getExtensionInfo(string $extension): string |
||||||
|
{ |
||||||
|
return json_encode( |
||||||
|
WasiDemo::extensionInfo($extension), |
||||||
|
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
:root { |
||||||
|
color-scheme: dark; |
||||||
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; |
||||||
|
background: #07111f; |
||||||
|
color: #eaf2ff; |
||||||
|
font-synthesis: none; |
||||||
|
} |
||||||
|
|
||||||
|
* { box-sizing: border-box; } |
||||||
|
body { margin: 0; min-width: 320px; min-height: 100vh; overflow-x: hidden; background: radial-gradient(circle at 50% -20%, #16345a 0, #07111f 42%, #040a13 100%); } |
||||||
|
button, input, textarea { font: inherit; } |
||||||
|
.ambient { position: fixed; width: 34rem; height: 34rem; border-radius: 50%; filter: blur(110px); opacity: .13; pointer-events: none; } |
||||||
|
.ambient-one { top: -12rem; right: -8rem; background: #35e0ff; } |
||||||
|
.ambient-two { bottom: -16rem; left: -10rem; background: #9d6cff; } |
||||||
|
.shell { position: relative; width: min(1180px, calc(100% - 40px)); margin: 0 auto; padding: 64px 0 36px; } |
||||||
|
.hero { display: grid; grid-template-columns: 1.2fr .8fr; align-items: center; min-height: 370px; gap: 40px; } |
||||||
|
.eyebrow, .section-label { margin: 0 0 12px; color: #79e8ff; font-size: .73rem; font-weight: 800; letter-spacing: .17em; text-transform: uppercase; } |
||||||
|
.pulse { display: inline-block; width: 8px; height: 8px; margin-right: 9px; border-radius: 50%; background: #6df5bd; box-shadow: 0 0 0 6px #6df5bd18; } |
||||||
|
h1 { margin: 0; font-size: clamp(4rem, 8vw, 7rem); font-weight: 760; letter-spacing: -.07em; line-height: .82; } |
||||||
|
h1 span { color: transparent; background: linear-gradient(90deg, #77eaff, #a789ff 62%, #f7a95c); background-clip: text; } |
||||||
|
.lede { max-width: 620px; margin: 28px 0 0; color: #9fb1ca; font-size: 1.08rem; line-height: 1.8; } |
||||||
|
.runtime-orbit { position: relative; width: 280px; height: 280px; margin: auto; display: grid; place-items: center; } |
||||||
|
.orbit-ring { position: absolute; inset: 18px; border: 1px solid #83e9ff40; border-radius: 50%; box-shadow: inset 0 0 60px #558cff0c; } |
||||||
|
.orbit-ring::before, .orbit-ring::after { content: ""; position: absolute; inset: 34px; border: 1px dashed #a789ff38; border-radius: 50%; } |
||||||
|
.orbit-ring::after { inset: -15px; border-style: solid; border-color: #ffffff0d; } |
||||||
|
.runtime-core { z-index: 1; display: grid; place-items: center; width: 120px; height: 120px; border: 1px solid #79e8ff55; border-radius: 32px; background: linear-gradient(145deg, #163552, #0b1b2c); color: #87ecff; text-align: center; box-shadow: 0 26px 80px #0008, inset 0 1px #ffffff18; transform: rotate(-5deg); } |
||||||
|
.runtime-core strong { color: #fff; font-size: 2rem; } |
||||||
|
.satellite { position: absolute; padding: 7px 11px; border: 1px solid #ffffff18; border-radius: 99px; background: #0c1929dd; color: #aebed3; font: 700 .66rem ui-monospace, monospace; } |
||||||
|
.satellite.one { top: 20px; left: 31px; }.satellite.two { right: 3px; top: 108px; }.satellite.three { bottom: 22px; left: 16px; } |
||||||
|
.panel { border: 1px solid #ffffff12; border-radius: 24px; background: linear-gradient(145deg, #0d1c2dcc, #091522d9); box-shadow: 0 25px 70px #00000035, inset 0 1px #ffffff09; backdrop-filter: blur(18px); } |
||||||
|
.control-panel { padding: 30px; } |
||||||
|
.panel-heading, .actions { display: flex; align-items: center; justify-content: space-between; gap: 24px; } |
||||||
|
h2 { margin: 0; font-size: 1.35rem; letter-spacing: -.025em; } |
||||||
|
.status { display: flex; align-items: center; gap: 9px; padding: 8px 13px; border-radius: 99px; background: #ffffff08; color: #9cafc6; font-size: .76rem; font-weight: 750; text-transform: uppercase; letter-spacing: .08em; } |
||||||
|
.status span { width: 7px; height: 7px; border-radius: 50%; background: currentColor; } |
||||||
|
.status.running { color: #ffd67a; }.status.running span { animation: blink .8s ease-in-out infinite alternate; }.status.success { color: #62edb2; }.status.error { color: #ff7d8d; } |
||||||
|
@keyframes blink { to { opacity: .25; } } |
||||||
|
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin: 26px 0; } |
||||||
|
label > span:first-child { display: block; margin: 0 0 8px; color: #c7d5e7; font-size: .82rem; font-weight: 680; } |
||||||
|
label.wide { grid-column: 1 / -1; } |
||||||
|
input, textarea { width: 100%; border: 1px solid #ffffff13; border-radius: 12px; outline: 0; background: #030b14a8; color: #eaf2ff; padding: 13px 15px; transition: border .2s, box-shadow .2s; } |
||||||
|
input:focus, textarea:focus { border-color: #66ddff70; box-shadow: 0 0 0 3px #56dfff0d; } |
||||||
|
textarea { resize: vertical; } |
||||||
|
label small { display: block; margin-top: 7px; color: #62758e; font-size: .7rem; } |
||||||
|
.actions { padding-top: 23px; border-top: 1px solid #ffffff0c; } |
||||||
|
.switch-row { display: flex; align-items: center; gap: 12px; cursor: pointer; } |
||||||
|
.switch-row input { position: absolute; opacity: 0; pointer-events: none; } |
||||||
|
.switch { position: relative; width: 44px; height: 24px; border-radius: 20px; background: #28384b; transition: .2s; } |
||||||
|
.switch i { position: absolute; width: 18px; height: 18px; left: 3px; top: 3px; border-radius: 50%; background: #8292a7; transition: .2s; } |
||||||
|
.switch-row input:checked + .switch { background: #27bb88; }.switch-row input:checked + .switch i { transform: translateX(20px); background: #fff; } |
||||||
|
.switch-row > span:last-child strong, .switch-row > span:last-child small { display: block; }.switch-row strong { font-size: .8rem; }.switch-row small { margin-top: 3px; color: #687b94; font-size: .68rem; } |
||||||
|
.button-row { display: flex; gap: 10px; }.button { border: 0; border-radius: 12px; padding: 12px 17px; color: #dce9f8; cursor: pointer; font-size: .8rem; font-weight: 750; transition: transform .18s, opacity .18s; }.button:hover { transform: translateY(-1px); }.button:disabled { opacity: .45; cursor: wait; transform: none; }.button.secondary { background: #ffffff0a; border: 1px solid #ffffff12; }.button.primary { min-width: 150px; display: flex; justify-content: space-between; background: linear-gradient(100deg, #25b9dc, #725ee8); box-shadow: 0 9px 30px #4584df2e; color: #fff; } |
||||||
|
.results { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin: 16px 0; } |
||||||
|
.feature-card { position: relative; min-height: 178px; padding: 21px; overflow: hidden; border: 1px solid #ffffff10; border-radius: 20px; background: #0a1725d9; } |
||||||
|
.feature-card::after { content: ""; position: absolute; width: 110px; height: 110px; right: -42px; bottom: -52px; border-radius: 50%; background: var(--accent); filter: blur(45px); opacity: .18; } |
||||||
|
.accent-cyan { --accent: #45defd; }.accent-violet { --accent: #9a78ff; }.accent-amber { --accent: #ffb860; }.accent-green { --accent: #4ee1a3; } |
||||||
|
.icon { display: grid; place-items: center; width: 35px; height: 35px; border-radius: 10px; background: color-mix(in srgb, var(--accent) 12%, transparent); color: var(--accent); font-size: 1.15rem; } |
||||||
|
.feature-card p { margin: 22px 0 7px; color: #70839c; font-size: .7rem; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }.feature-card h3 { margin: 0; overflow: hidden; color: #e8f1fe; font: 650 1.02rem ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }.feature-card small { display: block; margin-top: 8px; overflow: hidden; color: #667991; font-size: .68rem; text-overflow: ellipsis; white-space: nowrap; } |
||||||
|
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }.data-panel, .precision-panel { padding: 27px; } |
||||||
|
dl { margin: 22px 0 0; } dl div { display: grid; grid-template-columns: 92px 1fr; gap: 14px; padding: 13px 0; border-top: 1px solid #ffffff0b; } dt { color: #71849d; font: 700 .71rem ui-monospace, monospace; text-transform: uppercase; } dd { margin: 0; overflow-wrap: anywhere; color: #bfd0e5; font: .78rem/1.55 ui-monospace, monospace; } |
||||||
|
.extensions-panel { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 22px; margin-top: 16px; padding: 27px; } |
||||||
|
.extensions-panel > strong { color: #79e8ff; font: 700 .75rem ui-monospace, monospace; } |
||||||
|
.extension-list { display: flex; grid-column: 1 / -1; flex-wrap: wrap; gap: 8px; padding-top: 20px; border-top: 1px solid #ffffff0b; } |
||||||
|
.extension-list > span, .extension-button { padding: 7px 11px; border: 1px solid #62daf329; border-radius: 99px; background: #38c5e70c; color: #a9c9dc; font: 650 .7rem ui-monospace, monospace; } |
||||||
|
.extension-button { cursor: pointer; transition: border-color .18s, background .18s, color .18s, transform .18s; } |
||||||
|
.extension-button:hover, .extension-button:focus-visible { border-color: #79e8ff88; background: #38c5e71c; color: #e7faff; outline: 0; transform: translateY(-1px); } |
||||||
|
.extension-button.active { border-color: #a789ff99; background: #8e6fff20; color: #f0eaff; box-shadow: 0 0 0 3px #8e6fff0c; } |
||||||
|
.extension-detail { grid-column: 1 / -1; min-height: 112px; padding: 22px; border: 1px solid #ffffff0c; border-radius: 18px; background: #030b1470; } |
||||||
|
.extension-empty { display: flex; flex-direction: column; gap: 7px; color: #6f829a; font-size: .76rem; } |
||||||
|
.extension-empty strong { color: #a9bad0; font-size: .88rem; } |
||||||
|
.extension-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; margin-bottom: 20px; } |
||||||
|
.extension-detail-header h3 { margin: 0 0 6px; color: #eef6ff; font: 720 1.2rem ui-monospace, monospace; } |
||||||
|
.extension-detail-header p { margin: 0; color: #71849d; font: .72rem ui-monospace, monospace; } |
||||||
|
.extension-flags { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 7px; } |
||||||
|
.extension-flags span { padding: 5px 8px; border-radius: 7px; background: #ffffff09; color: #91a6c0; font: 650 .64rem ui-monospace, monospace; } |
||||||
|
.extension-groups { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; } |
||||||
|
.extension-group { min-width: 0; padding: 15px; border: 1px solid #ffffff0a; border-radius: 13px; background: #0915228a; } |
||||||
|
.extension-group h4 { margin: 0 0 11px; color: #7890ab; font-size: .68rem; letter-spacing: .09em; text-transform: uppercase; } |
||||||
|
.extension-items { display: flex; flex-wrap: wrap; gap: 6px; max-height: 180px; overflow: auto; } |
||||||
|
.extension-items code { max-width: 100%; padding: 4px 7px; overflow: hidden; border-radius: 6px; background: #ffffff07; color: #b9cadf; font: .66rem ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; } |
||||||
|
.extension-items dl { width: 100%; margin: 0; } |
||||||
|
.extension-items dl div { grid-template-columns: minmax(90px, .7fr) 1fr; padding: 7px 0; } |
||||||
|
.extension-items dl div:first-child { border-top: 0; } |
||||||
|
.extension-loading { color: #79e8ff; font: .76rem ui-monospace, monospace; } |
||||||
|
.console { margin-top: 16px; padding: 0; overflow: hidden; } .console summary { display: flex; justify-content: space-between; padding: 19px 24px; cursor: pointer; color: #a9bad0; font-size: .8rem; } kbd { border: 1px solid #ffffff12; border-radius: 6px; padding: 3px 7px; background: #ffffff08; color: #63778f; font: .65rem ui-monospace, monospace; }.console pre { max-height: 420px; margin: 0; padding: 23px; overflow: auto; border-top: 1px solid #ffffff0b; background: #02081099; color: #8fe9c0; font: .73rem/1.6 ui-monospace, monospace; } |
||||||
|
footer { display: flex; justify-content: space-between; gap: 20px; padding: 28px 4px 0; color: #50647d; font-size: .68rem; } |
||||||
|
@media (max-width: 820px) { .hero { grid-template-columns: 1fr; padding-bottom: 40px; }.runtime-orbit { display: none; }.results { grid-template-columns: 1fr 1fr; }.detail-grid, .extension-groups { grid-template-columns: 1fr; } } |
||||||
|
@media (max-width: 560px) { .shell { width: min(100% - 24px, 1180px); padding-top: 38px; }h1 { font-size: 3.6rem; }.control-panel { padding: 21px; }.form-grid, .results, .extensions-panel { grid-template-columns: 1fr; }.wide { grid-column: auto !important; }.actions, .panel-heading, footer { align-items: stretch; flex-direction: column; }.extension-list { grid-column: auto; }.button-row { width: 100%; }.button { flex: 1; } } |
||||||
@ -0,0 +1,202 @@ |
|||||||
|
import { |
||||||
|
_setStderr, |
||||||
|
_setStdin, |
||||||
|
_setStdout, |
||||||
|
} from '@bytecodealliance/preview2-shim/cli'; |
||||||
|
import { |
||||||
|
_getPreopens, |
||||||
|
_setFileData, |
||||||
|
types as filesystemTypes, |
||||||
|
} from '@bytecodealliance/preview2-shim/filesystem'; |
||||||
|
import { WASIShim } from '@bytecodealliance/preview2-shim/instantiation'; |
||||||
|
|
||||||
|
const encoder = new TextEncoder(); |
||||||
|
const decoder = new TextDecoder(); |
||||||
|
let runtime = null; |
||||||
|
let fileData = null; |
||||||
|
let persistent = false; |
||||||
|
let storageName = 'typephp-wasi-filesystem.json'; |
||||||
|
let extensionQueue = Promise.resolve(); |
||||||
|
|
||||||
|
function installMutableFilesystem(data) { |
||||||
|
const descriptorEntries = new WeakMap(); |
||||||
|
for (const [descriptor] of _getPreopens()) descriptorEntries.set(descriptor, data); |
||||||
|
|
||||||
|
function resolve(entry, guestPath) { |
||||||
|
for (const segment of String(guestPath).split('/')) { |
||||||
|
if (segment === '' || segment === '.') continue; |
||||||
|
if (segment === '..' || !entry?.dir?.[segment]) throw { tag: 'no-entry' }; |
||||||
|
entry = entry.dir[segment]; |
||||||
|
} |
||||||
|
return entry; |
||||||
|
} |
||||||
|
|
||||||
|
function remove(descriptor, guestPath, directory) { |
||||||
|
const root = descriptorEntries.get(descriptor); |
||||||
|
if (!root) throw { tag: 'bad-descriptor' }; |
||||||
|
const segments = String(guestPath).split('/').filter((segment) => segment !== '' && segment !== '.'); |
||||||
|
const name = segments.pop(); |
||||||
|
if (!name || name === '..' || segments.includes('..')) throw { tag: 'no-entry' }; |
||||||
|
const parent = resolve(root, segments.join('/')); |
||||||
|
const entry = parent?.dir?.[name]; |
||||||
|
if (!entry) throw { tag: 'no-entry' }; |
||||||
|
if (directory ? !entry.dir : entry.dir) throw { tag: directory ? 'not-directory' : 'is-directory' }; |
||||||
|
if (directory && Object.keys(entry.dir).length !== 0) throw { tag: 'not-empty' }; |
||||||
|
delete parent.dir[name]; |
||||||
|
} |
||||||
|
|
||||||
|
const descriptor = filesystemTypes.Descriptor.prototype; |
||||||
|
const openAt = descriptor.openAt; |
||||||
|
descriptor.openAt = function (...args) { |
||||||
|
const opened = openAt.apply(this, args); |
||||||
|
const parent = descriptorEntries.get(this); |
||||||
|
if (parent) descriptorEntries.set(opened, resolve(parent, args[1])); |
||||||
|
return opened; |
||||||
|
}; |
||||||
|
descriptor.unlinkFileAt = function (guestPath) { |
||||||
|
remove(this, guestPath, false); |
||||||
|
}; |
||||||
|
descriptor.removeDirectoryAt = function (guestPath) { |
||||||
|
remove(this, guestPath, true); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function outputHandler(stream) { |
||||||
|
return { |
||||||
|
write(bytes) { |
||||||
|
self.postMessage({ type: stream, data: decoder.decode(bytes, { stream: true }) }); |
||||||
|
return BigInt(bytes.byteLength); |
||||||
|
}, |
||||||
|
blockingFlush() {}, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function inputHandler(text) { |
||||||
|
const bytes = encoder.encode(text); |
||||||
|
let offset = 0; |
||||||
|
return { |
||||||
|
blockingRead(length) { |
||||||
|
if (offset >= bytes.byteLength) { |
||||||
|
throw { tag: 'closed' }; |
||||||
|
} |
||||||
|
const end = Math.min(offset + Number(length), bytes.byteLength); |
||||||
|
const chunk = bytes.slice(offset, end); |
||||||
|
offset = end; |
||||||
|
return chunk; |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function encodeFileData(value) { |
||||||
|
return JSON.stringify(value, (_key, item) => item instanceof Uint8Array |
||||||
|
? { typephpBytes: Array.from(item) } |
||||||
|
: item); |
||||||
|
} |
||||||
|
|
||||||
|
function decodeFileData(value) { |
||||||
|
return JSON.parse(value, (_key, item) => item && Array.isArray(item.typephpBytes) |
||||||
|
? new Uint8Array(item.typephpBytes) |
||||||
|
: item); |
||||||
|
} |
||||||
|
|
||||||
|
async function openPersistentFile(name) { |
||||||
|
if (!navigator.storage?.getDirectory) { |
||||||
|
throw new Error('OPFS is not available in this browser'); |
||||||
|
} |
||||||
|
const root = await navigator.storage.getDirectory(); |
||||||
|
const handle = await root.getFileHandle(name, { create: true }); |
||||||
|
if (typeof handle.createSyncAccessHandle !== 'function') { |
||||||
|
throw new Error('OPFS synchronous access requires a dedicated Worker'); |
||||||
|
} |
||||||
|
return handle.createSyncAccessHandle(); |
||||||
|
} |
||||||
|
|
||||||
|
async function loadFileData(storageName) { |
||||||
|
const access = await openPersistentFile(storageName); |
||||||
|
try { |
||||||
|
const size = access.getSize(); |
||||||
|
if (size === 0) { |
||||||
|
return { dir: {} }; |
||||||
|
} |
||||||
|
const bytes = new Uint8Array(size); |
||||||
|
access.read(bytes, { at: 0 }); |
||||||
|
return decodeFileData(decoder.decode(bytes)); |
||||||
|
} finally { |
||||||
|
access.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function saveFileData(storageName, fileData) { |
||||||
|
const access = await openPersistentFile(storageName); |
||||||
|
try { |
||||||
|
const bytes = encoder.encode(encodeFileData(fileData)); |
||||||
|
access.truncate(0); |
||||||
|
access.write(bytes, { at: 0 }); |
||||||
|
access.flush(); |
||||||
|
} finally { |
||||||
|
access.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function start(data) { |
||||||
|
try { |
||||||
|
if (typeof WebAssembly.Suspending !== 'function' |
||||||
|
|| typeof WebAssembly.promising !== 'function') { |
||||||
|
throw new Error('This browser does not support WebAssembly JSPI, which is required for blocking WASI I/O'); |
||||||
|
} |
||||||
|
persistent = data.persistent === true; |
||||||
|
storageName = String(data.storageName || 'typephp-wasi-filesystem.json'); |
||||||
|
fileData = persistent ? await loadFileData(storageName) : { dir: {} }; |
||||||
|
_setFileData(fileData); |
||||||
|
installMutableFilesystem(fileData); |
||||||
|
_setStdin(inputHandler(String(data.stdin || ''))); |
||||||
|
_setStdout(outputHandler('stdout')); |
||||||
|
_setStderr(outputHandler('stderr')); |
||||||
|
|
||||||
|
const args = ['typephp.wasm', ...(Array.isArray(data.args) ? data.args.map(String) : [])]; |
||||||
|
const env = data.env && typeof data.env === 'object' ? { ...data.env } : {}; |
||||||
|
env.TYPEPHP_FETCH_URL ??= new URL('/fetch-demo.json', self.location.href).href; |
||||||
|
const wasi = new WASIShim({ |
||||||
|
sandbox: { |
||||||
|
args, |
||||||
|
env, |
||||||
|
enableNetwork: true, |
||||||
|
}, |
||||||
|
}); |
||||||
|
const { instantiate } = await import('./generated/program.js'); |
||||||
|
const component = await instantiate(null, wasi.getImportObject()); |
||||||
|
runtime = await component.api.createRuntime(); |
||||||
|
const json = await runtime.getDemoReport( |
||||||
|
JSON.stringify(Array.isArray(data.args) ? data.args.map(String) : []), |
||||||
|
String(env.DEMO_GREETING || ''), |
||||||
|
String(data.stdin || ''), |
||||||
|
); |
||||||
|
if (persistent) { |
||||||
|
await saveFileData(storageName, fileData); |
||||||
|
} |
||||||
|
self.postMessage({ type: 'report', json }); |
||||||
|
} catch (error) { |
||||||
|
self.postMessage({ type: 'error', error: error?.stack || String(error) }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function getExtensionInfo(extension) { |
||||||
|
if (!runtime) { |
||||||
|
throw new Error('TypePHP runtime is not ready'); |
||||||
|
} |
||||||
|
const json = await runtime.getExtensionInfo(extension); |
||||||
|
self.postMessage({ type: 'extension-info', extension, json }); |
||||||
|
} |
||||||
|
|
||||||
|
self.onmessage = ({ data }) => { |
||||||
|
if (data?.type === 'run') { |
||||||
|
start(data); |
||||||
|
} else if (data?.type === 'extension-info') { |
||||||
|
const extension = String(data.extension || ''); |
||||||
|
extensionQueue = extensionQueue |
||||||
|
.then(() => getExtensionInfo(extension)) |
||||||
|
.catch((error) => { |
||||||
|
self.postMessage({ type: 'extension-error', extension, error: error?.stack || String(error) }); |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
import { defineConfig } from 'vite'; |
||||||
|
|
||||||
|
export default defineConfig({ |
||||||
|
server: { |
||||||
|
host: '127.0.0.1', |
||||||
|
}, |
||||||
|
worker: { |
||||||
|
format: 'es', |
||||||
|
}, |
||||||
|
}); |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
<?php |
||||||
|
$xml = simplexml_load_string('<root></root>'); |
||||||
|
var_dump($xml); |
||||||
|
var_dump((bool)$xml); |
||||||
|
|
||||||
|
$obj = new stdClass(); |
||||||
|
var_dump((bool) $obj); |
||||||
|
|
||||||
|
class UserClass {} |
||||||
|
|
||||||
|
$user = new UserClass(); |
||||||
|
var_dump((bool) $user); |
||||||
@ -1,25 +0,0 @@ |
|||||||
{ |
|
||||||
"name": "compiler", |
|
||||||
"version": "1.0.0", |
|
||||||
"description": "- 需要 PHP-8.2 以上版本 - 需要 GCC-9 以上版本,支持 C++17 标准 - 需要 CMake-3.24 以上版本", |
|
||||||
"main": "index.js", |
|
||||||
"directories": { |
|
||||||
"doc": "docs", |
|
||||||
"example": "examples", |
|
||||||
"test": "tests" |
|
||||||
}, |
|
||||||
"scripts": { |
|
||||||
"test": "echo \"Error: no test specified\" && exit 1" |
|
||||||
}, |
|
||||||
"repository": { |
|
||||||
"type": "git", |
|
||||||
"url": "git@git.code-galaxy.net:aot/compiler.git" |
|
||||||
}, |
|
||||||
"keywords": [], |
|
||||||
"author": "", |
|
||||||
"license": "ISC", |
|
||||||
"type": "commonjs", |
|
||||||
"dependencies": { |
|
||||||
"sharp": "^0.34.5" |
|
||||||
} |
|
||||||
} |
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue