diff --git a/ext/mbstring/JitMbOutputHandler.php b/ext/mbstring/JitMbOutputHandler.php new file mode 100644 index 0000000000..c3065db091 --- /dev/null +++ b/ext/mbstring/JitMbOutputHandler.php @@ -0,0 +1,139 @@ + $args + */ + public static function invoke(Context $context, array $args): Value + { + if (2 !== \count($args)) { + throw new \ArgumentCountError(sprintf( + 'mb_output_handler() expects exactly 2 arguments, %d given', + \count($args) + )); + } + + $stringLit = JitStringArg::compileTimeLiteral($args[0]); + $statusLit = self::compileTimeStatus($context, $args[1]); + if (null !== $stringLit && null !== $statusLit) { + return self::materializeString( + $context, + self::foldOutputHandler($context, $stringLit, $statusLit) + ); + } + + return self::lowerRuntime($context, $args[0]); + } + + private static function foldOutputHandler(Context $context, string $string, int $status): string + { + $httpOutput = MbstringAotFoldState::httpOutput($context) ?? (string) MbstringState::httpOutput(); + if (0 === strcasecmp($httpOutput, 'pass')) { + return $string; + } + $from = MbstringAotFoldState::internalEncoding($context) ?? MbstringState::internalEncoding(); + if (0 === strcasecmp($from, $httpOutput)) { + return $string; + } + $converted = VmMbstring::convertEncoding($string, $httpOutput, $from); + if (false === $converted) { + return $string; + } + + return $converted; + } + + private static function lowerRuntime(Context $context, JITVariable $stringArg): Value + { + $savedInsert = BasicBlockHelper::tryGetInsertBlock($context); + MbHttpOutputRuntime::ensureLinked($context); + MbInternalEncodingRuntime::ensureLinked($context); + MbOutputHandlerRuntime::ensureLinked($context); + MbConvertEncodingRuntime::ensureLinked($context); + if (null !== $savedInsert) { + BasicBlockHelper::restoreInsertBlock($context, $savedInsert); + } + BasicBlockHelper::ensureOpenInsertBlock($context, 'mb_output_handler_runtime'); + + $string = JitStringBuiltinArg::lower( + $context, + $stringArg, + 'mb_output_handler', + 0, + 'string' + ); + + $httpCode = $context->builder->load(MbHttpOutputRuntime::encodingCodeGlobal($context)); + $internalCode = $context->builder->load(MbInternalEncodingRuntime::encodingCodeGlobal($context)); + $raw = JitNestedHelperCoerce::callHelper( + $context, + MbOutputHandlerRuntime::convertHelper($context), + [$string, $httpCode, $internalCode] + ); + $resultStr = JitNestedHelperCoerce::extractStringPtrFromHelperResult($context, $raw); + + return self::materializeOwnedString($context, $resultStr); + } + + private static function compileTimeStatus(Context $context, JITVariable $arg): ?int + { + if (JITVariable::TYPE_NATIVE_LONG === $arg->type && JITVariable::KIND_VALUE === $arg->kind) { + $lib = $context->llvm->lib; + if (null !== $lib->LLVMIsAConstantInt($arg->value->value)) { + return (int) $lib->LLVMConstIntGetSExtValue($arg->value->value); + } + } + $constName = $arg->compileTimeConstantName ?? null; + if (null !== $constName && null !== $context->runtime->vmContext) { + $phpVar = $context->runtime->vmContext->constantFetch($constName); + if (null !== $phpVar && \PHPCompiler\VM\Variable::TYPE_INTEGER === $phpVar->type) { + return $phpVar->toInt(); + } + } + + return null; + } + + private static function materializeString(Context $context, string $str): Value + { + return self::materializeOwnedString( + $context, + $context->builder->load($context->constantStringFromString($str)) + ); + } + + private static function materializeOwnedString(Context $context, Value $resultStr): Value + { + $owned = $context->builder->call($context->lookupFunction('__string__separate'), $resultStr); + $slot = JitValueBox::alloc($context); + $ptr = JitValueBox::pointer($context, $slot); + $context->builder->call($context->lookupFunction('__value__writeString'), $ptr, $owned); + + return $ptr; + } +} diff --git a/ext/mbstring/MbOutputHandlerJitHelper.php b/ext/mbstring/MbOutputHandlerJitHelper.php new file mode 100644 index 0000000000..e8a283db0c --- /dev/null +++ b/ext/mbstring/MbOutputHandlerJitHelper.php @@ -0,0 +1,42 @@ + 'UTF-8', + MbHttpOutputJitHelper::CODE_ASCII => 'ASCII', + MbHttpOutputJitHelper::CODE_ISO88591 => 'ISO-8859-1', + MbHttpOutputJitHelper::CODE_SJIS => 'SJIS', + MbHttpOutputJitHelper::CODE_EUCJP => 'EUC-JP', + MbHttpOutputJitHelper::CODE_8BIT => '8BIT', + MbHttpOutputJitHelper::CODE_PASS => 'pass', + default => 'UTF-8', + }; + } + + public static function convertArgv(string $string, int $httpCode, int $internalCode): string + { + if (MbHttpOutputJitHelper::CODE_PASS === $httpCode) { + return $string; + } + $http = self::nameFromCodeArgv($httpCode); + $from = self::nameFromCodeArgv($internalCode); + if (0 === strcasecmp($from, $http)) { + return $string; + } + + return MbConvertEncodingJitHelper::convertArgv($string, $http, $from); + } +} diff --git a/ext/mbstring/mb_output_handler.php b/ext/mbstring/mb_output_handler.php index 62f5916a93..885120c4d3 100644 --- a/ext/mbstring/mb_output_handler.php +++ b/ext/mbstring/mb_output_handler.php @@ -7,13 +7,14 @@ use PHPCompiler\Frame; use PHPCompiler\Func\Internal; use PHPCompiler\JIT\Context; -use PHPCompiler\JIT\JitStringArg; use PHPCompiler\JIT\Variable as JITVariable; use PHPLLVM\Value; /** * mb_output_handler() — OB callback converting to mb_http_output encoding * (php-src ext/mbstring/mbstring.c; #20014). + * + * JIT/AOT: compile-time fold + NestedJIT via {@see JitMbOutputHandler}. */ final class mb_output_handler extends Internal { @@ -49,59 +50,6 @@ public function execute(Frame $frame): void public function call(Context $context, JITVariable ...$args): Value { - if (2 !== \count($args)) { - throw new \ArgumentCountError(sprintf( - 'mb_output_handler() expects exactly 2 arguments, %d given', - \count($args) - )); - } - - $stringLit = JitStringArg::compileTimeLiteral($args[0]); - $statusLit = self::compileTimeStatus($context, $args[1]); - if (null === $stringLit || null === $statusLit) { - throw new \LogicException( - 'mb_output_handler() requires compile-time string and int arguments in this compiler build' - ); - } - $out = self::foldOutputHandler($context, $stringLit, $statusLit); - - return $context->builder->load($context->constantStringFromString($out)); - } - - private static function foldOutputHandler(Context $context, string $string, int $status): string - { - $httpOutput = MbstringAotFoldState::httpOutput($context) ?? (string) MbstringState::httpOutput(); - if (0 === strcasecmp($httpOutput, 'pass')) { - return $string; - } - $from = MbstringAotFoldState::internalEncoding($context) ?? MbstringState::internalEncoding(); - if (0 === strcasecmp($from, $httpOutput)) { - return $string; - } - $converted = VmMbstring::convertEncoding($string, $httpOutput, $from); - if (false === $converted) { - return $string; - } - - return $converted; - } - - private static function compileTimeStatus(Context $context, JITVariable $arg): ?int - { - if (JITVariable::TYPE_NATIVE_LONG === $arg->type && JITVariable::KIND_VALUE === $arg->kind) { - $lib = $context->llvm->lib; - if (null !== $lib->LLVMIsAConstantInt($arg->value->value)) { - return (int) $lib->LLVMConstIntGetSExtValue($arg->value->value); - } - } - $constName = $arg->compileTimeConstantName ?? null; - if (null !== $constName && null !== $context->runtime->vmContext) { - $phpVar = $context->runtime->vmContext->constantFetch($constName); - if (null !== $phpVar && \PHPCompiler\VM\Variable::TYPE_INTEGER === $phpVar->type) { - return $phpVar->toInt(); - } - } - - return null; + return JitMbOutputHandler::invoke($context, $args); } } diff --git a/lib/JIT/Builtin/MbOutputHandlerRuntime.php b/lib/JIT/Builtin/MbOutputHandlerRuntime.php new file mode 100644 index 0000000000..7e29da7301 --- /dev/null +++ b/lib/JIT/Builtin/MbOutputHandlerRuntime.php @@ -0,0 +1,74 @@ + */ + private const COMPILED_HELPERS = [ + self::CONVERT_LOGICAL, + ]; + + public static function ensureLinked(Context $context): void + { + self::ensureJitHelperCompiled($context); + self::ensureOutconvGlobal($context); + } + + public static function convertHelper(Context $context): LlvmFunction + { + self::ensureJitHelperCompiled($context); + + return JitVmHelperLink::lookupCompiled($context, self::CONVERT_LOGICAL, '#20014'); + } + + public static function outconvGlobal(Context $context): Value + { + self::ensureOutconvGlobal($context); + $g = $context->module->getNamedGlobal(self::G_OUTCONV_ENABLED); + if (null === $g) { + throw new \LogicException(self::G_OUTCONV_ENABLED.' missing (#20014)'); + } + + return $g; + } + + private static function ensureOutconvGlobal(Context $context): void + { + if (null !== $context->module->getNamedGlobal(self::G_OUTCONV_ENABLED)) { + return; + } + $i64 = $context->getTypeFromString('int64'); + $g = $context->module->addGlobal($i64, self::G_OUTCONV_ENABLED); + $g->setInitializer($i64->constInt(0, false)); + } + + private static function ensureJitHelperCompiled(Context $context): void + { + JitVmHelperLink::ensureCompiled( + $context, + self::HELPER_PATH, + self::COMPILED_HELPERS, + 'mb_output_handler', + true + ); + } +} diff --git a/test/repro/aot_mb_output_handler_runtime.php b/test/repro/aot_mb_output_handler_runtime.php new file mode 100644 index 0000000000..3e0c82c978 --- /dev/null +++ b/test/repro/aot_mb_output_handler_runtime.php @@ -0,0 +1,17 @@ +markTestSkipped('LLVM 9 toolchain not available'); + } + $this->assertAotMatchesZend(__DIR__.'/../repro/aot_mb_output_handler_runtime.php'); + } + + public function testHelperAndLoweringPresent(): void + { + $root = dirname(__DIR__, 2); + $helper = (string) file_get_contents($root.'/ext/mbstring/MbOutputHandlerJitHelper.php'); + $this->assertStringContainsString('function convertArgv', $helper); + $this->assertStringContainsString('MbConvertEncodingJitHelper::convertArgv', $helper); + $runtime = (string) file_get_contents($root.'/lib/JIT/Builtin/MbOutputHandlerRuntime.php'); + $this->assertStringContainsString('MbOutputHandlerJitHelper::convertArgv', $runtime); + $jit = (string) file_get_contents($root.'/ext/mbstring/JitMbOutputHandler.php'); + $this->assertStringContainsString('MbOutputHandlerRuntime::ensureLinked', $jit); + $src = (string) file_get_contents($root.'/ext/mbstring/mb_output_handler.php'); + $this->assertStringContainsString('JitMbOutputHandler::invoke', $src); + $this->assertStringNotContainsString( + 'requires compile-time string and int arguments', + $src + ); + $this->assertFileDoesNotExist($root.'/lib/AOT/runtime/mb_output_handler.c'); + } + + private function assertAotMatchesZend(string $src): void + { + $zend = $this->runPhp($src); + $aot = $this->runAot($src); + $this->assertSame($zend, $aot); + } + + private function runPhp(string $src): string + { + $cmd = escapeshellarg(PHP_BINARY).' '.escapeshellarg($src); + exec($cmd.' 2>/dev/null', $out, $rc); + $this->assertSame(0, $rc, implode("\n", $out)); + + return implode("\n", $out); + } + + private function runAot(string $src): string + { + $root = dirname(__DIR__, 2); + $bin = sys_get_temp_dir().'/mb_output_handler_'.getmypid().'_'.md5($src); + $cmd = 'env PHP_COMPILER_HELPER_RUNTIME_O=0 ' + .escapeshellarg(PHP_BINARY).' ' + .escapeshellarg($root.'/bin/compile.php') + .' -o '.escapeshellarg($bin).' '.escapeshellarg($src); + $cwd = getcwd(); + chdir($root); + try { + exec($cmd.' 2>&1', $compOut, $compRc); + $this->assertSame(0, $compRc, implode("\n", $compOut)); + $this->assertFileExists($bin); + exec(escapeshellarg($bin).' 2>/dev/null', $out, $rc); + $this->assertSame(0, $rc, implode("\n", $out)); + + return implode("\n", $out); + } finally { + chdir($cwd); + @unlink($bin); + } + } +}