From 222911cbde9736402a0207f8c620557cf3bbfb68 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 15 Sep 2026 15:43:29 +0800 Subject: [PATCH] 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 --- .github/smoke/linux-arm64/.gitignore | 2 + phpunit/src/Backend/BackendTest.php | 11 ++ .../src/Build/IncrementalDeclarationTest.php | 106 ++++++++++++++++++ src/Backend/Msvc.php | 3 +- src/Build/IncrementalCompilationTrait.php | 30 ++++- src/Build/SourcePipelineTrait.php | 10 +- src/Translator.php | 46 +++++++- 7 files changed, 195 insertions(+), 13 deletions(-) create mode 100644 .github/smoke/linux-arm64/.gitignore diff --git a/.github/smoke/linux-arm64/.gitignore b/.github/smoke/linux-arm64/.gitignore new file mode 100644 index 00000000..3ad3780e --- /dev/null +++ b/.github/smoke/linux-arm64/.gitignore @@ -0,0 +1,2 @@ +/build +/linux_arm64_smoke \ No newline at end of file diff --git a/phpunit/src/Backend/BackendTest.php b/phpunit/src/Backend/BackendTest.php index d55c67d5..d5734e7c 100644 --- a/phpunit/src/Backend/BackendTest.php +++ b/phpunit/src/Backend/BackendTest.php @@ -117,6 +117,17 @@ class BackendTest extends TestCase $this->assertStringNotContainsString('/std:', $cmd); } + public function testMsvcLargeObjectFormatAppliesToGeneratedAndNativeCommands(): void + { + $compiler = new Msvc(new Windows()); + foreach ([false, true] as $debug) { + $options = ['debug' => $debug]; + $this->assertStringContainsString('/bigobj', $compiler->buildCompileCommand('generated.cc', 'generated.obj', $options)); + $this->assertStringContainsString('/bigobj', $compiler->buildNativeCompileCommand('native.c', 'native.obj', $options, 'c')); + } + $this->assertStringNotContainsString('/bigobj', $compiler->buildLinkOptions()); + } + public function testMsvcDebugPdbOptionsApplyToCppAndCCommands(): void { $compiler = new Msvc(new Windows()); diff --git a/phpunit/src/Build/IncrementalDeclarationTest.php b/phpunit/src/Build/IncrementalDeclarationTest.php index a63c53c4..f22d03a3 100644 --- a/phpunit/src/Build/IncrementalDeclarationTest.php +++ b/phpunit/src/Build/IncrementalDeclarationTest.php @@ -316,6 +316,112 @@ PHP); self::assertDirectoryDoesNotExist($incrementalDirectory); } + private function prepareNativeStub(): void + { + $stub = $this->directory . '/provider.stub.php'; + rename($this->provider, $stub); + $this->provider = $stub; + file_put_contents($stub, 'consumer, 'prepareNativeStub(); + $first = $this->convertProject(); + $header = $first->getDeclarationHeaderFile($this->provider); + self::assertFileExists($header); + self::assertStringContainsString('php_incremental__answer(', file_get_contents($header)); + self::assertStringContainsString('#include <' . basename($header) . '>', + file_get_contents($first->getDeclarationHeaderFile($this->consumer))); + self::assertFileDoesNotExist($this->invoke($first, 'getCppFile', $this->provider)); + self::assertFileExists($first->getArgInfoHeaderFile($this->provider)); + $extension = $this->buildDirectory . '/extension-incremental.cc'; + $extensionCode = file_get_contents($extension); + self::assertStringContainsString('ZEND_FUNCTION(incremental_answer)', $extensionCode); + self::assertStringContainsString('#include <' . basename($first->getArgInfoHeaderFile($this->provider)) . '>', $extensionCode); + $state = json_decode(file_get_contents($this->buildDirectory . '/cache/incremental/incremental/build-state.json'), true); + self::assertFalse($state['files'][$this->provider]['emitsTranslationUnit']); + self::assertContains($this->provider, $state['files'][$this->consumer]['dependencies']); + $second = $this->convertProject(); + self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider)); + self::assertFalse($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer)); + self::assertSame($extensionCode, file_get_contents($extension)); + } + + public function testStubChangePreservingMtimeInvalidatesTheConsumer(): void + { + $this->prepareNativeStub(); + $this->convertProject(); + $extension = $this->buildDirectory . '/extension-incremental.cc'; + $oldCode = file_get_contents($extension); + $mtime = filemtime($this->provider); + file_put_contents($this->provider, str_replace('42', '43', file_get_contents($this->provider))); + touch($this->provider, $mtime); + clearstatcache(); + $second = $this->convertProject(); + self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->provider)); + self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer)); + self::assertNotSame($oldCode, file_get_contents($extension)); + self::assertStringContainsString('43', file_get_contents($extension)); + } + + public function testMissingStubHeaderIsRegeneratedAndInvalidatesConsumers(): void + { + $this->prepareNativeStub(); + $first = $this->convertProject(); + unlink($first->getDeclarationHeaderFile($this->provider)); + $second = $this->convertProject(); + self::assertFileExists($second->getDeclarationHeaderFile($this->provider)); + self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer)); + } + + public function testMissingStubArginfoIsRegeneratedAndInvalidatesConsumers(): void + { + $this->prepareNativeStub(); + $first = $this->convertProject(); + unlink($first->getArgInfoHeaderFile($this->provider)); + $second = $this->convertProject(); + self::assertFileExists($second->getArgInfoHeaderFile($this->provider)); + self::assertTrue($this->invoke($second, 'shouldRegeneratePhpFile', $this->consumer)); + } + + public function testImportedClassMetadataAndCallbacksStayInTheExtension(): void + { + $this->prepareNativeStub(); + file_put_contents($this->provider, <<<'PHP' +consumer, <<<'PHP' +add(1) + \Incremental\answer(); +} +PHP); + $first = $this->convertProject(); + $arginfo = $first->getArgInfoHeaderFile($this->provider); + $extension = $this->buildDirectory . '/extension-incremental.cc'; + $extensionCode = file_get_contents($extension); + self::assertStringContainsString('php_register_class_Incremental_Counter', file_get_contents($arginfo)); + self::assertStringContainsString('ZEND_METHOD(Incremental_Counter, add)', $extensionCode); + self::assertStringContainsString('php_incremental__counter__add(this_, arg_delta)', $extensionCode); + self::assertStringContainsString('#include <' . basename($arginfo) . '>', $extensionCode); + self::assertStringNotContainsString(basename($arginfo), file_get_contents($this->invoke($first, 'getCppFile', $this->consumer))); + self::assertFileDoesNotExist($this->invoke($first, 'getCppFile', $this->provider)); + $this->convertProject(); + self::assertSame($extensionCode, file_get_contents($extension)); + } + private function convertProject(): CompilerTest { global $translator; diff --git a/src/Backend/Msvc.php b/src/Backend/Msvc.php index b0eeaf85..0cc85a75 100644 --- a/src/Backend/Msvc.php +++ b/src/Backend/Msvc.php @@ -36,7 +36,8 @@ class Msvc extends CompilerBackend private function buildCommonCompileFlags(array $config, bool $includeCppOptions = true): string { - $cmd = ''; + // Generated code/templates can exceed ordinary COFF section limits. + $cmd = ' /bigobj'; $cmd .= ' /utf-8 /DZEND_WIN32 /DPHP_WIN32 /DZEND_DEBUG=0 /DENABLE_INTSAFE_SIGNED_FUNCTIONS'; diff --git a/src/Build/IncrementalCompilationTrait.php b/src/Build/IncrementalCompilationTrait.php index 36b1d589..038220eb 100644 --- a/src/Build/IncrementalCompilationTrait.php +++ b/src/Build/IncrementalCompilationTrait.php @@ -20,6 +20,26 @@ trait IncrementalCompilationTrait private string $incrementalGeneratorFingerprint = ''; private bool $incrementalPlanInitialized = false; + /** + * Stubs provide declarations without emitting a body translation unit. + * Include them in header generation and dependency invalidation as well. + * @param list $files + * @return list + */ + private function getDeclarationInputFiles(array $files): array + { + $inputs = []; + foreach ($files as $file) { + $inputs[realpath($file) ?: $file] = true; + } + foreach ($this->preparedFileAsts as $file => $_ast) { + if ($this->isStubFile($file)) { + $inputs[$file] = true; + } + } + return array_keys($inputs); + } + /** @param list $files */ protected function initializeIncrementalCompilation(array $files): void { @@ -29,7 +49,7 @@ trait IncrementalCompilationTrait $this->incrementalDirtyFiles = []; $phpFiles = []; - foreach ($files as $file) { + foreach ($this->getDeclarationInputFiles($files) as $file) { if (!FileScanner::isPhpFile($file)) { continue; } @@ -112,7 +132,7 @@ trait IncrementalCompilationTrait if (!$this->incrementalPlanInitialized) { return; } - foreach ($files as $file) { + foreach ($this->getDeclarationInputFiles($files) as $file) { if (!FileScanner::isPhpFile($file)) { continue; } @@ -198,7 +218,7 @@ trait IncrementalCompilationTrait } $this->rebuildIncrementalGlobalState(); $phpFiles = []; - foreach ($files as $file) { + foreach ($this->getDeclarationInputFiles($files) as $file) { if (!FileScanner::isPhpFile($file)) { continue; } @@ -218,7 +238,7 @@ trait IncrementalCompilationTrait } $this->rebuildIncrementalGlobalState(); $stateFiles = []; - foreach ($files as $file) { + foreach ($this->getDeclarationInputFiles($files) as $file) { if (!FileScanner::isPhpFile($file)) { continue; } @@ -248,7 +268,7 @@ trait IncrementalCompilationTrait 'symbolsDeclared' => $declared, 'symbolsUsed' => $used, 'emitsTranslationUnit' => $this->incrementalTranslationUnits[$path] - ?? (bool) ($previous['emitsTranslationUnit'] ?? false), + ?? (!$this->isStubFile($path) && (bool) ($previous['emitsTranslationUnit'] ?? false)), 'header' => $this->getDeclarationHeaderFile($path), 'cpp' => $this->getCppFile($path), 'splitTranslationUnits' => $this->getSplitTranslationUnits($path), diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index cdfe53d9..763ca1df 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -368,7 +368,7 @@ trait SourcePipelineTrait // All declarations are now known. Lower declaration constant // expressions before translating any function body so cache IDs // are assigned exclusively in the convert phase. - $this->finalizeDeclarationExpressions($files); + $this->finalizeDeclarationExpressions($this->getDeclarationInputFiles($files)); // Whole-program extension generation must not depend on conversion // side effects from dirty files. Clean incremental files are not // converted, but their non-empty property defaults still require a @@ -377,6 +377,14 @@ trait SourcePipelineTrait $this->initializeDeclarationHeaderFiles($files); $this->restoreCleanIncrementalMetadata($files); + // Native/import stubs are declaration inputs, not ordinary PHP + // bodies. Their Zend metadata still belongs to the module entry. + foreach ($this->getDeclarationInputFiles($files) as $file) { + if ($this->isStubFile($file) && $this->shouldRegeneratePhpFile($file)) { + $this->genStubFile($file); + } + } + $sourceFiles = []; $validSourceCount = 0; // Generate the C++ files diff --git a/src/Translator.php b/src/Translator.php index 46122864..7554c335 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1193,6 +1193,10 @@ class Translator extends Preprocessor sort($this->registerSymbols, SORT_STRING); sort($this->releaseAstConstantFns, SORT_STRING); $this->localHeaders = $this->argInfoHeaderFiles; + // Stubs have native implementations, but still need local Zend + // callbacks. Generate these before sizing caches/rendering literals: + // parameter validation may allocate additional stable cache IDs. + $stubWrapperCode = $this->genStubWrappers(); // genExtension() is also a public code-generation entry used directly // by tooling and tests, outside SourcePipelineTrait::convert(). Keep the // whole-program property-default invariant local to the consumer too. @@ -1395,6 +1399,9 @@ CODE; $code .= '} // namespace ' . $projectNamespace . PHP_EOL . PHP_EOL; + $code .= "// native/import stub callbacks \n"; + $code .= $stubWrapperCode; + $code .= "// default argument values \n"; $code .= $this->genDefaultArgumentHelperDefinitions(); @@ -2770,7 +2777,7 @@ CODE; private function initializeDeclarationHeaderFiles(array $files): void { $this->declarationHeaderFiles = []; - foreach ($files as $file) { + foreach ($this->getDeclarationInputFiles($files) as $file) { if (FileScanner::isPhpFile($file)) { $this->declarationHeaderFiles[$file] = $this->getDeclarationHeaderFile($file, true); } @@ -2831,17 +2838,14 @@ CODE; $this->writeFile($runtimeHeader, '#pragma once' . PHP_EOL . PHP_EOL . $this->renderDataDeclarations(null, true) . $this->genNativeObjectForwardDeclarations()); - foreach ($files as $file) { - if (!isset($this->declarationHeaderFiles[$file])) { - continue; - } + foreach ($this->declarationHeaderFiles as $file => $header) { if (!$this->shouldRegeneratePhpFile($file)) { continue; } $code = $this->renderFunctionDeclarations($file); $code .= $this->renderDataDeclarations($file); $this->writeFile( - $this->getIncludeDir() . '/' . $this->declarationHeaderFiles[$file], + $this->getIncludeDir() . '/' . $header, $code, $this->shouldRegeneratePhpFile($file), ); @@ -3060,6 +3064,13 @@ CODE; ]; } else { $declarationHeaders = [$this->getRuntimeDeclarationHeaderName()]; + // The callbacks emitted here bridge stub declarations to native + // C++ implementations. Arginfo remains included only by this TU. + foreach ($this->declarationHeaderFiles as $source => $header) { + if ($this->isStubFile($source)) { + $declarationHeaders[] = $header; + } + } // Generated arginfo registration helpers call compile-time // attribute factories directly to materialize lazy values such as // enum cases. Include only the declaration owners of those helper @@ -8410,6 +8421,29 @@ CODE; return $cppCode; } + private function genStubWrappers(): string + { + $code = ''; + foreach ($this->symbols->classes() as $classDef) { + if (!$this->isStubFile($classDef->sourceFile) || $classDef->nativeObject || $classDef->trait !== null) { + continue; + } + foreach ($classDef->methods as $methodDef) { + if (!$methodDef->functionDef->abstractMethod + && !$this->functionUsesNativeObject($methodDef->functionDef)) { + $code .= $this->genMethodWrapper($classDef, $methodDef); + } + } + } + foreach ($this->symbols->functions() as $functionDef) { + if ($functionDef->stub && !$functionDef->method && !$functionDef->attributeFactory + && !$this->functionUsesNativeObject($functionDef)) { + $code .= $this->genFunctionWrapper($functionDef); + } + } + return $code; + } + /** Return the generated C++ symbol for a hidden runtime-attribute factory. */ public function getRuntimeAttributeFactoryNativeName(string $fullName): string {