Skip to content

Commit 222911c

Browse files
committed
feat(build): support stub files in incremental compilation
- Add getDeclarationInputFiles method to include stub files in header generation - Modify incremental compilation to process stub files alongside regular PHP files - Update build state tracking to handle stub files differently (no translation units) - Add test coverage for stub declaration generation and dependency invalidation - Implement MSVC /bigobj flag for large object format support in generated commands - Generate wrapper callbacks for stub functions and methods bridging to native implementations - Update source pipeline to handle native/import stub metadata in module entry - Modify declaration header generation to include stub callback registrations - Add comprehensive test suite for stub functionality
1 parent 4aba302 commit 222911c

7 files changed

Lines changed: 195 additions & 13 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/build
2+
/linux_arm64_smoke

phpunit/src/Backend/BackendTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,17 @@ public function testMsvcBuildCCompileCommandKeepsSharedCompilerOptions(): void
117117
$this->assertStringNotContainsString('/std:', $cmd);
118118
}
119119

120+
public function testMsvcLargeObjectFormatAppliesToGeneratedAndNativeCommands(): void
121+
{
122+
$compiler = new Msvc(new Windows());
123+
foreach ([false, true] as $debug) {
124+
$options = ['debug' => $debug];
125+
$this->assertStringContainsString('/bigobj', $compiler->buildCompileCommand('generated.cc', 'generated.obj', $options));
126+
$this->assertStringContainsString('/bigobj', $compiler->buildNativeCompileCommand('native.c', 'native.obj', $options, 'c'));
127+
}
128+
$this->assertStringNotContainsString('/bigobj', $compiler->buildLinkOptions());
129+
}
130+
120131
public function testMsvcDebugPdbOptionsApplyToCppAndCCommands(): void
121132
{
122133
$compiler = new Msvc(new Windows());

phpunit/src/Build/IncrementalDeclarationTest.php

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,112 @@ public function testForceCacheClearRemovesAstAndTargetIncrementalState(): void
316316
self::assertDirectoryDoesNotExist($incrementalDirectory);
317317
}
318318

319+
private function prepareNativeStub(): void
320+
{
321+
$stub = $this->directory . '/provider.stub.php';
322+
rename($this->provider, $stub);
323+
$this->provider = $stub;
324+
file_put_contents($stub, '<?php namespace Incremental; function answer(int $value = 42): int {}');
325+
file_put_contents($this->consumer, '<?php function main(): int { return \\Incremental\\answer(); }');
326+
}
327+
328+
public function testStubDeclarationsAreGeneratedWithoutConvertingStubBodies(): void
329+
{
330+
$this->prepareNativeStub();
331+
$first = $this->convertProject();
332+
$header = $first->getDeclarationHeaderFile($this->provider);
333+
self::assertFileExists($header);
334+
self::assertStringContainsString('php_incremental__answer(', file_get_contents($header));
335+
self::assertStringContainsString('#include <' . basename($header) . '>',
336+
file_get_contents($first->getDeclarationHeaderFile($this->consumer)));
337+
self::assertFileDoesNotExist($this->invoke($first, 'getCppFile', $this->provider));
338+
self::assertFileExists($first->getArgInfoHeaderFile($this->provider));
339+
$extension = $this->buildDirectory . '/extension-incremental.cc';
340+
$extensionCode = file_get_contents($extension);
341+
self::assertStringContainsString('ZEND_FUNCTION(incremental_answer)', $extensionCode);
342+
self::assertStringContainsString('#include <' . basename($first->getArgInfoHeaderFile($this->provider)) . '>', $extensionCode);
343+
$state = json_decode(file_get_contents($this->buildDirectory . '/cache/incremental/incremental/build-state.json'), true);
344+
self::assertFalse($state['files'][$this->provider]['emitsTranslationUnit']);
345+
self::assertContains($this->provider, $state['files'][$this->consumer]['dependencies']);
346+
$second = $this->convertProject();
347+
self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider));
348+
self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
349+
self::assertSame($extensionCode, file_get_contents($extension));
350+
}
351+
352+
public function testStubChangePreservingMtimeInvalidatesTheConsumer(): void
353+
{
354+
$this->prepareNativeStub();
355+
$this->convertProject();
356+
$extension = $this->buildDirectory . '/extension-incremental.cc';
357+
$oldCode = file_get_contents($extension);
358+
$mtime = filemtime($this->provider);
359+
file_put_contents($this->provider, str_replace('42', '43', file_get_contents($this->provider)));
360+
touch($this->provider, $mtime);
361+
clearstatcache();
362+
$second = $this->convertProject();
363+
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider));
364+
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
365+
self::assertNotSame($oldCode, file_get_contents($extension));
366+
self::assertStringContainsString('43', file_get_contents($extension));
367+
}
368+
369+
public function testMissingStubHeaderIsRegeneratedAndInvalidatesConsumers(): void
370+
{
371+
$this->prepareNativeStub();
372+
$first = $this->convertProject();
373+
unlink($first->getDeclarationHeaderFile($this->provider));
374+
$second = $this->convertProject();
375+
self::assertFileExists($second->getDeclarationHeaderFile($this->provider));
376+
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
377+
}
378+
379+
public function testMissingStubArginfoIsRegeneratedAndInvalidatesConsumers(): void
380+
{
381+
$this->prepareNativeStub();
382+
$first = $this->convertProject();
383+
unlink($first->getArgInfoHeaderFile($this->provider));
384+
$second = $this->convertProject();
385+
self::assertFileExists($second->getArgInfoHeaderFile($this->provider));
386+
self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer));
387+
}
388+
389+
public function testImportedClassMetadataAndCallbacksStayInTheExtension(): void
390+
{
391+
$this->prepareNativeStub();
392+
file_put_contents($this->provider, <<<'PHP'
393+
<?php
394+
/** @import-library */
395+
namespace Incremental;
396+
function answer(int $value = 42): int {}
397+
final class Counter
398+
{
399+
public int $value = 0;
400+
public function add(int $delta): int {}
401+
}
402+
PHP);
403+
file_put_contents($this->consumer, <<<'PHP'
404+
<?php
405+
function main(): int
406+
{
407+
$counter = new \Incremental\Counter();
408+
return $counter->add(1) + \Incremental\answer();
409+
}
410+
PHP);
411+
$first = $this->convertProject();
412+
$arginfo = $first->getArgInfoHeaderFile($this->provider);
413+
$extension = $this->buildDirectory . '/extension-incremental.cc';
414+
$extensionCode = file_get_contents($extension);
415+
self::assertStringContainsString('php_register_class_Incremental_Counter', file_get_contents($arginfo));
416+
self::assertStringContainsString('ZEND_METHOD(Incremental_Counter, add)', $extensionCode);
417+
self::assertStringContainsString('php_incremental__counter__add(this_, arg_delta)', $extensionCode);
418+
self::assertStringContainsString('#include <' . basename($arginfo) . '>', $extensionCode);
419+
self::assertStringNotContainsString(basename($arginfo), file_get_contents($this->invoke($first, 'getCppFile', $this->consumer)));
420+
self::assertFileDoesNotExist($this->invoke($first, 'getCppFile', $this->provider));
421+
$this->convertProject();
422+
self::assertSame($extensionCode, file_get_contents($extension));
423+
}
424+
319425
private function convertProject(): CompilerTest
320426
{
321427
global $translator;

src/Backend/Msvc.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public function getLinkerCommand(): string
3636

3737
private function buildCommonCompileFlags(array $config, bool $includeCppOptions = true): string
3838
{
39-
$cmd = '';
39+
// Generated code/templates can exceed ordinary COFF section limits.
40+
$cmd = ' /bigobj';
4041

4142
$cmd .= ' /utf-8 /DZEND_WIN32 /DPHP_WIN32 /DZEND_DEBUG=0 /DENABLE_INTSAFE_SIGNED_FUNCTIONS';
4243

src/Build/IncrementalCompilationTrait.php

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,26 @@ trait IncrementalCompilationTrait
2020
private string $incrementalGeneratorFingerprint = '';
2121
private bool $incrementalPlanInitialized = false;
2222

23+
/**
24+
* Stubs provide declarations without emitting a body translation unit.
25+
* Include them in header generation and dependency invalidation as well.
26+
* @param list<string> $files
27+
* @return list<string>
28+
*/
29+
private function getDeclarationInputFiles(array $files): array
30+
{
31+
$inputs = [];
32+
foreach ($files as $file) {
33+
$inputs[realpath($file) ?: $file] = true;
34+
}
35+
foreach ($this->preparedFileAsts as $file => $_ast) {
36+
if ($this->isStubFile($file)) {
37+
$inputs[$file] = true;
38+
}
39+
}
40+
return array_keys($inputs);
41+
}
42+
2343
/** @param list<string> $files */
2444
protected function initializeIncrementalCompilation(array $files): void
2545
{
@@ -29,7 +49,7 @@ protected function initializeIncrementalCompilation(array $files): void
2949
$this->incrementalDirtyFiles = [];
3050

3151
$phpFiles = [];
32-
foreach ($files as $file) {
52+
foreach ($this->getDeclarationInputFiles($files) as $file) {
3353
if (!FileScanner::isPhpFile($file)) {
3454
continue;
3555
}
@@ -112,7 +132,7 @@ protected function restoreCleanIncrementalMetadata(array $files): void
112132
if (!$this->incrementalPlanInitialized) {
113133
return;
114134
}
115-
foreach ($files as $file) {
135+
foreach ($this->getDeclarationInputFiles($files) as $file) {
116136
if (!FileScanner::isPhpFile($file)) {
117137
continue;
118138
}
@@ -198,7 +218,7 @@ protected function finalizeIncrementalConversionMetadata(array $files): void
198218
}
199219
$this->rebuildIncrementalGlobalState();
200220
$phpFiles = [];
201-
foreach ($files as $file) {
221+
foreach ($this->getDeclarationInputFiles($files) as $file) {
202222
if (!FileScanner::isPhpFile($file)) {
203223
continue;
204224
}
@@ -218,7 +238,7 @@ protected function saveIncrementalCompilationState(array $files): void
218238
}
219239
$this->rebuildIncrementalGlobalState();
220240
$stateFiles = [];
221-
foreach ($files as $file) {
241+
foreach ($this->getDeclarationInputFiles($files) as $file) {
222242
if (!FileScanner::isPhpFile($file)) {
223243
continue;
224244
}
@@ -248,7 +268,7 @@ protected function saveIncrementalCompilationState(array $files): void
248268
'symbolsDeclared' => $declared,
249269
'symbolsUsed' => $used,
250270
'emitsTranslationUnit' => $this->incrementalTranslationUnits[$path]
251-
?? (bool) ($previous['emitsTranslationUnit'] ?? false),
271+
?? (!$this->isStubFile($path) && (bool) ($previous['emitsTranslationUnit'] ?? false)),
252272
'header' => $this->getDeclarationHeaderFile($path),
253273
'cpp' => $this->getCppFile($path),
254274
'splitTranslationUnits' => $this->getSplitTranslationUnits($path),

src/Build/SourcePipelineTrait.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ public function convert(array $files): array
368368
// All declarations are now known. Lower declaration constant
369369
// expressions before translating any function body so cache IDs
370370
// are assigned exclusively in the convert phase.
371-
$this->finalizeDeclarationExpressions($files);
371+
$this->finalizeDeclarationExpressions($this->getDeclarationInputFiles($files));
372372
// Whole-program extension generation must not depend on conversion
373373
// side effects from dirty files. Clean incremental files are not
374374
// converted, but their non-empty property defaults still require a
@@ -377,6 +377,14 @@ public function convert(array $files): array
377377
$this->initializeDeclarationHeaderFiles($files);
378378
$this->restoreCleanIncrementalMetadata($files);
379379

380+
// Native/import stubs are declaration inputs, not ordinary PHP
381+
// bodies. Their Zend metadata still belongs to the module entry.
382+
foreach ($this->getDeclarationInputFiles($files) as $file) {
383+
if ($this->isStubFile($file) && $this->shouldRegeneratePhpFile($file)) {
384+
$this->genStubFile($file);
385+
}
386+
}
387+
380388
$sourceFiles = [];
381389
$validSourceCount = 0;
382390
// Generate the C++ files

src/Translator.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,10 @@ private function doGenExtension(): string
11931193
sort($this->registerSymbols, SORT_STRING);
11941194
sort($this->releaseAstConstantFns, SORT_STRING);
11951195
$this->localHeaders = $this->argInfoHeaderFiles;
1196+
// Stubs have native implementations, but still need local Zend
1197+
// callbacks. Generate these before sizing caches/rendering literals:
1198+
// parameter validation may allocate additional stable cache IDs.
1199+
$stubWrapperCode = $this->genStubWrappers();
11961200
// genExtension() is also a public code-generation entry used directly
11971201
// by tooling and tests, outside SourcePipelineTrait::convert(). Keep the
11981202
// whole-program property-default invariant local to the consumer too.
@@ -1395,6 +1399,9 @@ private function doGenExtension(): string
13951399

13961400
$code .= '} // namespace ' . $projectNamespace . PHP_EOL . PHP_EOL;
13971401

1402+
$code .= "// native/import stub callbacks \n";
1403+
$code .= $stubWrapperCode;
1404+
13981405
$code .= "// default argument values \n";
13991406
$code .= $this->genDefaultArgumentHelperDefinitions();
14001407

@@ -2770,7 +2777,7 @@ private function getAllDeclarationHeaderName(): string
27702777
private function initializeDeclarationHeaderFiles(array $files): void
27712778
{
27722779
$this->declarationHeaderFiles = [];
2773-
foreach ($files as $file) {
2780+
foreach ($this->getDeclarationInputFiles($files) as $file) {
27742781
if (FileScanner::isPhpFile($file)) {
27752782
$this->declarationHeaderFiles[$file] = $this->getDeclarationHeaderFile($file, true);
27762783
}
@@ -2831,17 +2838,14 @@ private function genDeclarationHeaders(array $files): void
28312838
$this->writeFile($runtimeHeader, '#pragma once' . PHP_EOL . PHP_EOL
28322839
. $this->renderDataDeclarations(null, true)
28332840
. $this->genNativeObjectForwardDeclarations());
2834-
foreach ($files as $file) {
2835-
if (!isset($this->declarationHeaderFiles[$file])) {
2836-
continue;
2837-
}
2841+
foreach ($this->declarationHeaderFiles as $file => $header) {
28382842
if (!$this->shouldRegeneratePhpFile($file)) {
28392843
continue;
28402844
}
28412845
$code = $this->renderFunctionDeclarations($file);
28422846
$code .= $this->renderDataDeclarations($file);
28432847
$this->writeFile(
2844-
$this->getIncludeDir() . '/' . $this->declarationHeaderFiles[$file],
2848+
$this->getIncludeDir() . '/' . $header,
28452849
$code,
28462850
$this->shouldRegeneratePhpFile($file),
28472851
);
@@ -3060,6 +3064,13 @@ private function genExtensionIncludeHeaderFiles(): string
30603064
];
30613065
} else {
30623066
$declarationHeaders = [$this->getRuntimeDeclarationHeaderName()];
3067+
// The callbacks emitted here bridge stub declarations to native
3068+
// C++ implementations. Arginfo remains included only by this TU.
3069+
foreach ($this->declarationHeaderFiles as $source => $header) {
3070+
if ($this->isStubFile($source)) {
3071+
$declarationHeaders[] = $header;
3072+
}
3073+
}
30633074
// Generated arginfo registration helpers call compile-time
30643075
// attribute factories directly to materialize lazy values such as
30653076
// enum cases. Include only the declaration owners of those helper
@@ -8410,6 +8421,29 @@ private function genFunctionWrapper(FunctionDef $functionDef): string
84108421
return $cppCode;
84118422
}
84128423

8424+
private function genStubWrappers(): string
8425+
{
8426+
$code = '';
8427+
foreach ($this->symbols->classes() as $classDef) {
8428+
if (!$this->isStubFile($classDef->sourceFile) || $classDef->nativeObject || $classDef->trait !== null) {
8429+
continue;
8430+
}
8431+
foreach ($classDef->methods as $methodDef) {
8432+
if (!$methodDef->functionDef->abstractMethod
8433+
&& !$this->functionUsesNativeObject($methodDef->functionDef)) {
8434+
$code .= $this->genMethodWrapper($classDef, $methodDef);
8435+
}
8436+
}
8437+
}
8438+
foreach ($this->symbols->functions() as $functionDef) {
8439+
if ($functionDef->stub && !$functionDef->method && !$functionDef->attributeFactory
8440+
&& !$this->functionUsesNativeObject($functionDef)) {
8441+
$code .= $this->genFunctionWrapper($functionDef);
8442+
}
8443+
}
8444+
return $code;
8445+
}
8446+
84138447
/** Return the generated C++ symbol for a hidden runtime-attribute factory. */
84148448
public function getRuntimeAttributeFactoryNativeName(string $fullName): string
84158449
{

0 commit comments

Comments
 (0)