diff --git a/phpunit/src/EntryScriptCodegenTest.php b/phpunit/src/EntryScriptCodegenTest.php new file mode 100644 index 00000000..39bcf607 --- /dev/null +++ b/phpunit/src/EntryScriptCodegenTest.php @@ -0,0 +1,69 @@ +projectDir = sys_get_temp_dir() . '/typephp_entry_script_' . bin2hex(random_bytes(6)); + mkdir($this->projectDir, 0777, true); + } + + protected function tearDown(): void + { + $this->removeDirectory($this->projectDir); + parent::tearDown(); + } + + public function testMainLinePaddingDoesNotBloatGeneratedExtension(): void + { + $source = $this->projectDir . '/main.php'; + file_put_contents( + $source, + "projectDir); + $translator = $compiler; + $compiler->setBuildMode(CompilerBase::BUILD_MODE_BIN); + $compiler->setTargetName('entry_script'); + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $compiler->convertFile($source); + + $extension = file_get_contents($compiler->genExtension()); + + self::assertStringContainsString( + 'php::eval(std::string(257, \'\\n\') + "main();",', + $extension, + ); + self::assertStringNotContainsString('php::eval("\\n', $extension); + } + + private function removeDirectory(string $directory): void + { + if (!is_dir($directory)) { + return; + } + + foreach (array_diff(scandir($directory), ['.', '..']) as $entry) { + $path = $directory . '/' . $entry; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($directory); + } +} diff --git a/src/Translator.php b/src/Translator.php index f407e5fa..c6429060 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1214,14 +1214,24 @@ CODE; // CLI script fields always identify main()'s canonical absolute file. $entryFile = $entryFunction->sourceFile; $entryFileArg = $this->genCharPtr($entryFile, true); - $entryPrefix = str_repeat("\n", max(0, $entryFunction->startLine - 1)); + $entryLineOffset = max(0, $entryFunction->startLine - 1); if (count($entryFunction->argInfoList) == 2) { - $entryScript = $entryPrefix . 'global $argc, $argv; main($argc, $argv);'; + $entryScript = 'global $argc, $argv; main($argc, $argv);'; } else { - $entryScript = $entryPrefix . 'main();'; + $entryScript = 'main();'; } - $code .= 'php::eval(' . $this->genCharPtr($entryScript, true) . ', ' . $entryFileArg . ');' . PHP_EOL; + $entryScriptArg = $this->genCharPtr($entryScript, true); + if ($entryLineOffset > 0) { + // entryLineOffset is main()'s source start line minus one. The + // generated std::string(N, '\n') supplies N padding newlines at + // runtime, so the eval() entry call is reported on main()'s + // original PHP source line. Constructing the padding at runtime + // avoids embedding hundreds of escaped newlines in the C++ file. + $entryScriptArg = 'std::string(' . $entryLineOffset . ", '\\n') + " . $entryScriptArg; + } + + $code .= 'php::eval(' . $entryScriptArg . ', ' . $entryFileArg . ');' . PHP_EOL; } $code .= 'return SUCCESS;' . PHP_EOL;