diff --git a/src/Generator/ParamTypes/ParamParser.php b/src/Generator/ParamTypes/ParamParser.php new file mode 100644 index 00000000..264bc7da --- /dev/null +++ b/src/Generator/ParamTypes/ParamParser.php @@ -0,0 +1,57 @@ + $argInfos + * @return string + */ + public function createZendParamTypeCheck(array $argInfos): string + { + // Return the default macro if no parameter information is available. + if (empty($argInfos)) { + return sprintf("do { %s } while(false);", self::NONE_PARAMS_TEMPLATE); + } + + [$minNumArgs, $maxNumArgs] = $this->getRequiredAndAllParamCounts($argInfos); + $zendVariables = (new ZendVariables())->createZendVariables($argInfos); + $zendParamMarcos = (new ZendParamMacros())->createZendParamMacros($argInfos, $minNumArgs, $maxNumArgs); + + return sprintf( + "do { %s %s } while(false);", + implode(PHP_EOL, $zendVariables), + implode(PHP_EOL, $zendParamMarcos)); + } + + /** + * Calculate the count of required parameters and total parameters. + * @param array $argInfos + * @return array + */ + private function getRequiredAndAllParamCounts(array $argInfos): array + { + $argCount = count($argInfos); + $lastIndex = $argCount - 1; + + if ($argCount > 0 && $argInfos[$lastIndex]->variadic) { + return [$lastIndex, -1]; + } + + $requiredParamCount = count(array_filter($argInfos, fn($argInfo) => $argInfo->defaultValue === null)); + return [$requiredParamCount, $argCount]; + } +} diff --git a/src/Generator/ParamTypes/ZendParamMacros.php b/src/Generator/ParamTypes/ZendParamMacros.php new file mode 100644 index 00000000..82ce44fe --- /dev/null +++ b/src/Generator/ParamTypes/ZendParamMacros.php @@ -0,0 +1,198 @@ + $argInfos + * @param int $minNumArgs + * @param int $maxNumArgs + * @return array + */ + public function createZendParamMacros(array $argInfos, int $minNumArgs, int $maxNumArgs): array + { + $marcos[] = $this->genStartParseCode($minNumArgs, $maxNumArgs); + foreach ($argInfos as $index => $argInfo) { + if ($maxNumArgs > $minNumArgs && $index == $minNumArgs) { + $marcos[] = $this->genOptionalParseCode(); + } + + $marcos[] = match ($argInfo->type) { + Type::BOOL => $this->parseBool($argInfo), + Type::INT => $this->parseInt($argInfo), + Type::FLOAT => $this->parseFloat($argInfo), + Type::ARRAY => $this->parseArray($argInfo), + Type::STR => $this->parseString($argInfo), + Type::REF => $this->parseReference($argInfo), + Type::RESOURCE, Type::STREAM => $this->parseResource($argInfo), + Type::OBJECT => $this->parseObject($argInfo, $argInfo->class), + default => $this->parseVar($argInfo) + }; + } + + $marcos[] = $this->genEndParseCode(); + return $marcos; + } + + /** + * ZEND_PARAM_PARSE start + * @param int $minNumArgs + * @param int $maxNumArgs + * @return string + */ + private function genStartParseCode(int $minNumArgs, int $maxNumArgs): string + { + return sprintf("ZEND_PARSE_PARAMETERS_START(%d, %d)", $minNumArgs, $maxNumArgs); + } + + /** + * ZEND_PARAM_PARSE end + * @return string + */ + private function genEndParseCode(): string + { + return "ZEND_PARSE_PARAMETERS_END_EX(php::throwErrorIfOccurred());"; + } + + /** + * Z_PARAM_OPTIONAL + * @return string + */ + private function genOptionalParseCode(): string + { + return "Z_PARAM_OPTIONAL"; + } + + private function parseBool(ArgInfo $argInfo): string + { + $name = $argInfo->name; + return $argInfo->nullable + ? sprintf("Z_PARAM_BOOL_OR_NULL(%s, %s_is_null)", $name, $name) + : "Z_PARAM_BOOL($name)"; + } + + private function parseInt(ArgInfo $argInfo): string + { + $name = $argInfo->name; + return $argInfo->nullable + ? sprintf("Z_PARAM_LONG_OR_NULL(%s, %s_is_null)", $name, $name) + : "Z_PARAM_LONG($name)"; + } + + private function parseFloat(ArgInfo $argInfo): string + { + $name = $argInfo->name; + return $argInfo->nullable + ? sprintf("Z_PARAM_DOUBLE_OR_NULL(%s, %s_is_null)", $name, $name) + : "Z_PARAM_DOUBLE($name)"; + } + + private function parseArray(ArgInfo $argInfo): string + { + $name = $argInfo->name; + return $argInfo->nullable ? "Z_PARAM_ARRAY_OR_NULL($name)" : "Z_PARAM_ARRAY($name)"; + } + + private function parseString(ArgInfo $argInfo): string + { + $name = $argInfo->name; + return $argInfo->nullable ? "Z_PARAM_STR_OR_NULL($name)" : "Z_PARAM_STR($name)"; + } + + private function parseArgs(ArgInfo $argInfo): string + { + $name = $argInfo->name; + return sprintf("Z_PARAM_VARIADIC(*, %s_argv, %s_argc)", $name, $name); + } + + private function parseReference(ArgInfo $argInfo): string + { + return "Z_PARAM_ZVAL($argInfo->name)"; + } + + private function parseResource(ArgInfo $argInfo): string + { + return "Z_PARAM_RESOURCE($argInfo->name)"; + } + + private function parseObject(ArgInfo $argInfo, string $className): string + { + $name = $argInfo->name; + if ($className) { + return $argInfo->nullable + ? sprintf("Z_PARAM_OBJECT_OF_CLASS_OR_NULL(%s_obj, %s_ce)", $name, $name) + : sprintf("Z_PARAM_OBJECT_OF_CLASS(%s_obj, %s_ce)", $name, $name); + } + + return $argInfo->nullable ? "Z_PARAM_OBJECT_OR_NULL($name)" : "Z_PARAM_OBJECT($name)"; + } + + /** + * The compiler does not currently support the callable type. + * For now, we'll implement the method first and fill in the callable support later. + * @param \TypePhp\Entity\ArgInfo $argInfo + * @return string + */ + private function parseCallable(ArgInfo $argInfo): string + { + $name = $argInfo->name; + return $argInfo->nullable + ? sprintf("Z_PARAM_FUNC_OR_NULL(%s_fci, %s_fcc)", $name, $name) + : sprintf("Z_PARAM_FUNC(%s_fci, %s_fcc)", $name, $name); + } + + /** + * For multiple types, we only handle plain types, mixed, and nullable variables. + * The remaining UnionType and IntersectionType are too complex to be processed via macros. + * @param \TypePhp\Entity\ArgInfo $argInfo + * @return string + */ + private function parseVar(ArgInfo $argInfo): string + { + if ($argInfo->variadic) { + return $this->parseArgs($argInfo); + } + + if ($argInfo->byRef) { + return $this->parseArgs($argInfo); + } + + $name = $argInfo->name; + if (!$argInfo->typeNode instanceof NullableType) { + return $argInfo->nullable ? "Z_PARAM_ZVAL_OR_NULL($name)" : "Z_PARAM_ZVAL($name)"; + } + + /** + * Nullable only contains Identifier, FullyQualified, and Relative. + * The Relative option is not supported by the compiler. + */ + $type = $argInfo->typeNode->type; + if ($type instanceof Identifier) { + return match ($type->name) { + "int" => $this->parseInt($argInfo), + "string" => $this->parseString($argInfo), + "array" => $this->parseArray($argInfo), + "bool" => $this->parseBool($argInfo), + "float" => $this->parseFloat($argInfo), + "object" => $this->parseObject($argInfo, $argInfo->class), + /** + * A default case is not strictly required here, since the PHP-supported types are already listed above. + * However, it is included anyway to keep the code robust in case new types are introduced in the future. + */ + default => "Z_PARAM_ZVAL_OR_NULL($name)" + }; + } + + // FullyQualified + return $this->parseObject($argInfo, $type->toString()); + } +} diff --git a/src/Generator/ParamTypes/ZendVariables.php b/src/Generator/ParamTypes/ZendVariables.php new file mode 100644 index 00000000..ab562e5e --- /dev/null +++ b/src/Generator/ParamTypes/ZendVariables.php @@ -0,0 +1,140 @@ + $argInfos + * @return array + */ + public function createZendVariables(array $argInfos): array + { + $variables = []; + foreach ($argInfos as $argInfo) { + $variables[] = match ($argInfo->type) { + Type::BOOL => $this->genBoolVariable($argInfo->name), + Type::INT => $this->genIntVariable($argInfo->name), + Type::FLOAT => $this->genFloatVariable($argInfo->name), + Type::ARRAY => $this->genArrayVariable($argInfo->name), + Type::STR => $this->genStringVariable($argInfo->name), + Type::RESOURCE, Type::STREAM => $this->genResourceVariable($argInfo->name), + Type::OBJECT => $this->genObjectVariable($argInfo->name, $argInfo->class), + default => $this->genVarVariable($argInfo), + }; + } + + return $variables; + } + + private function genBoolVariable(string $name): string + { + return sprintf("zend_bool %s = 0; zend_bool %s_is_null = false;", $name, $name); + } + + private function genIntVariable(string $name): string + { + return sprintf("zend_long %s = 0; zend_bool %s_is_null = false;", $name, $name); + } + + private function genFloatVariable(string $name): string + { + return sprintf("double %s = 0; zend_bool %s_is_null = false;", $name, $name); + } + + private function genArrayVariable(string $name): string + { + return sprintf("zval *%s = nullptr;", $name); + } + + private function genStringVariable(string $name): string + { + return sprintf("zend_string *%s = nullptr;", $name); + } + + private function genArgsVariable(string $name): string + { + return sprintf("zval *%s_argv = nullptr; int %s_argc = 0;", $name, $name); + } + + private function genReferenceVariable(string $name): string + { + return sprintf("zval *%s = nullptr;", $name); + } + + private function genResourceVariable(string $name): string + { + return sprintf("zval *%s = nullptr;", $name); + } + + private function genObjectVariable(string $name, string $className): string + { + if ($className) { + return sprintf(<<variadic) { + return $this->genArgsVariable($argInfo->name); + } + + if ($argInfo->byRef) { + return $this->genReferenceVariable($argInfo->name); + } + + if (!$argInfo->typeNode instanceof NullableType) { + return sprintf("zval *%s = nullptr;", $argInfo->name); + } + + /** @var NullableType $typeNode */ + $typeNode = $argInfo->typeNode; + $type = $typeNode->type; + $name = $argInfo->name; + + /** + * Nullable only contains Identifier, FullyQualified, and Relative. + * The Relative option is not supported by the compiler. + */ + if ($type instanceof Identifier) { + return match ($type->name) { + 'bool' => $this->genBoolVariable($name), + 'int' => $this->genIntVariable($name), + 'string' => $this->genStringVariable($name), + 'float' => $this->genFloatVariable($name), + 'array' => $this->genArrayVariable($name), + 'object' => $this->genObjectVariable($name, ''), + default => sprintf("zval *%s = nullptr;", $name), + }; + } + + // FullyQualified + return $this->genObjectVariable($name, $type->toString()); + } +} \ No newline at end of file diff --git a/src/Translator.php b/src/Translator.php index 52b80885..f1cfd844 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -11,16 +11,21 @@ namespace TypePhp; use Ajaxray\AnsiKit\AnsiTerminal; use Ajaxray\AnsiKit\Components\Progressbar; use MJS\TopSort\Implementations\StringSort; +use PhpParser\Modifiers; +use PhpParser\Node; +use PhpParser\NodeAbstract; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; use TypePhp\Analysis\SsaBuilder; use TypePhp\Backend\CompilerFactory; use TypePhp\Build\FileScanner; -use TypePhp\Build\NativeCommandOptionsTrait; use TypePhp\Build\NativeBuilder; +use TypePhp\Build\NativeCommandOptionsTrait; use TypePhp\Build\PrecompiledHeaderManager; +use TypePhp\Build\ResourceCompilationTrait; use TypePhp\Build\SourcePipelineTrait; use TypePhp\Config\ProjectYamlLoader; use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic; -use TypePhp\Build\ResourceCompilationTrait; use TypePhp\Entity\ArgInfo; use TypePhp\Entity\ClassDef; use TypePhp\Entity\ClassLikeDef; @@ -34,21 +39,17 @@ use TypePhp\Exception\Skip; use TypePhp\Exception\SyntaxError; use TypePhp\Generator\DefaultArgumentGenerator; use TypePhp\Generator\LibraryImportStubGenerator; +use TypePhp\Generator\ParamTypes\ParamParser; use TypePhp\Generator\Symbol; use TypePhp\Metadata\Constants; use TypePhp\Platform\PlatformFactory; use TypePhp\Platform\Windows; -use TypePhp\Resolver\Reflection; use TypePhp\Resolver\ClassConstantValueTrait; -use TypePhp\Transform\Visitor; -use TypePhp\Transform\ConstructorLowering; +use TypePhp\Resolver\Reflection; use TypePhp\Transform\ConstantExpressionValidationVisitor; +use TypePhp\Transform\ConstructorLowering; use TypePhp\Transform\RuntimeAttributeFactoryLowering; -use PhpParser\Modifiers; -use PhpParser\Node; -use PhpParser\NodeAbstract; -use PhpParser\NodeTraverser; -use PhpParser\NodeVisitor\NameResolver; +use TypePhp\Transform\Visitor; class Translator extends Preprocessor { @@ -92,6 +93,7 @@ class Translator extends Preprocessor */ protected array $classCeList = []; protected array $classCeInfo = []; + private bool $declareStrictTypes = false; protected function isConstructorNativeFunction(FunctionDef $func): bool { @@ -2339,6 +2341,8 @@ CODE; $this->resetClass(); $this->resetMethod(); $this->resetFunction(); + // 重置$this->declareStrictTypes,并不是每份文件都设置了declare(strict_types=1),防止污染下一份文件 + $this->setDeclareStrictTypes(false); $cppCode = ''; foreach ($stmts as $v) { @@ -2494,6 +2498,9 @@ CODE; if (!($declare->value instanceof Node\Scalar\Int_) or $declare->value->value !== 1) { $this->fatalError($v, 'declare(strict_types=0) is not allowed, only strict_types=1 is supported'); } + + // 当前文件设置了declare(strict_types=1),我们需要保存这个状态,然后生成对应的代码。 + $this->setDeclareStrictTypes(true); } else { $this->fatalError($v, 'declare(' . $key . '=' . $value . ') is not supported'); } @@ -3167,9 +3174,15 @@ CODE; { $cppCode = ''; $callParams = ''; - if ($functionDef->argCountRequired > 0) { - $cppCode .= $this->genWrapperRequiredArgCountCheck($functionDef, $displayName); - } + + /** + * 若当前文件声明了 declare(strict_types=1);,则需为当前函数标记 ZEND_ACC_STRICT_TYPES 标志。 + * 该标志用于确保函数在通过 ZendVM 调用其他函数时,能够严格按照声明模式校验参数类型。 + * 需要注意的是,此机制仅作用于 ZendVM 动态调用的函数, + * 若目标函数为编译后的本地代码,则不会触发类型检查——这是 ZendVM 的设计使然,其哲学是信任已编译内部函数的 C 代码实现。 + */ + $cppCode .= $this->genDeclareStrictTypesCode(); + foreach ($functionDef->argInfoList as $k => $argInfo) { $var = 'arg_' . $argInfo->name; if ($argInfo->variadic) { @@ -3298,6 +3311,10 @@ CODE; { $name = $classDef->getNamespacedName(); $cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL; + + $paramParser = new ParamParser(); + $cppCode .= $paramParser->createZendParamTypeCheck($methodDef->functionDef->argInfoList); + $cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL; $fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef); $implicitMethodArgs = []; @@ -3524,6 +3541,7 @@ CODE; $this->indentLevel++; $code .= $this->genScopeVarDecl(); $code .= "\n"; + // Runtime union/nullable parameter type checks foreach ($this->functionDef->argInfoList as $i => $argInfo) { if (!empty($argInfo->typeCheck)) { @@ -4780,6 +4798,10 @@ CODE; { $name = $this->escapeZendFnName($functionDef->getNamespacedName()); $cppCode = 'ZEND_FUNCTION(' . $name . '){' . PHP_EOL; + + $paramParser = new ParamParser(); + $cppCode .= $paramParser->createZendParamTypeCheck($functionDef->argInfoList); + $fn = self::PREFIX . $this->getNativeName($functionDef->name, $functionDef->namespace); $cppCode .= $this->genWrapperFunctionArgs($fn, $functionDef, $functionDef->getNamespacedName()); @@ -4882,4 +4904,21 @@ CODE; return $code; } + + private function genDeclareStrictTypesCode(): string + { + /** + * 类型检查仅在二进制模式下启用。由于二进制模式作为独立可执行程序运行,需保持和直接运行的PHP代码一致的行为语义,因此必须保留类型检查以确保安全性。 + * 相反,当编译为扩展(如 ext/curl)时,编译端无需强制内置类型检查,其开启与否应由调用方PHP代码自行决定。 + */ + return $this->declareStrictTypes && $this->isBuildModeBin() + ? "execute_data->func->common.fn_flags |= ZEND_ACC_STRICT_TYPES;" . PHP_EOL + : ''; + } + + private function setDeclareStrictTypes(bool $type): void + { + $this->declareStrictTypes = $type; + } + }