diff --git a/src/Generator/ParamParser.php b/src/Generator/ParamParser.php new file mode 100644 index 00000000..c3d103b4 --- /dev/null +++ b/src/Generator/ParamParser.php @@ -0,0 +1,237 @@ + $argInfos + * @return string + */ + public function create(array $argInfos): string + { + if (empty($argInfos)) { + return sprintf("do { %s } while(false);", self::NONE_PARAMS_TEMPLATE).PHP_EOL; + } + + [$minNumArgs, $maxNumArgs] = $this->getRequiredAndOptionalParamCounts($argInfos); + $macros[] = $this->genStartParseCode($minNumArgs, $maxNumArgs); + + foreach ($argInfos as $index => $argInfo) { + if ($minNumArgs == 0 || $minNumArgs < $maxNumArgs && $index > $minNumArgs - 1) { + $macros[] = $this->genOptionalDirective(); + } + + $macros[] = $this->genBodyParserCode($argInfo); + } + + $macros[] = $this->genEndParseCode(); + return sprintf( + "do { %s %s } while(false);", + implode(PHP_EOL, $this->paramNames), + implode(PHP_EOL, $macros)); + } + + /** + * @return array + */ + public function getComplexTypes(): array + { + return $this->complexTypes; + } + + /** + * @param array $argInfos + * @return array + */ + private function getRequiredAndOptionalParamCounts(array $argInfos): array + { + if (count($argInfos) == 1 && $argInfos[0]->type === Type::REF) { + return [0, -1]; + } + + $hasRef = false; + foreach ($argInfos as $index => $argInfo) { + if ($argInfo->type === Type::REF) { + $hasRef = true; + break; + } + } + + $requiredParamCount = count(array_filter($argInfos, fn($argInfo) => $argInfo->defaultValue === null)); + return [$requiredParamCount, $hasRef ? -1 : count($argInfos)]; + } + + private function genStartParseCode(int $minNumArgs, int $maxNumArgs): string + { + return sprintf("ZEND_PARSE_PARAMETERS_START(%d, %d)", $minNumArgs, $maxNumArgs); + } + + private function genOptionalDirective(): string + { + return "Z_PARAM_OPTIONAL"; + } + + private function genBodyParserCode(ArgInfo $argInfo): string + { + return 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::ARGS => $this->parseArgs($argInfo), + Type::REF => $this->parseReference($argInfo), + Type::RESOURCE, Type::STREAM => $this->parseResource($argInfo), + Type::OBJECT => $this->parseObject($argInfo), + Type::VAR => $this->parseVar($argInfo), + default => '' + }; + } + + private function genEndParseCode(): string + { + return "ZEND_PARSE_PARAMETERS_END_EX(php::throwErrorIfOccurred());"; + } + + private function parseBool(ArgInfo $argInfo): string + { + $name = $argInfo->name; + $isNull = $name.'_is_null'; + + $this->paramNames[] = "zend_bool $name = false; zend_bool $isNull = false;"; + + return $argInfo->nullable + ? "Z_PARAM_BOOL_OR_NULL($name, $isNull)" + : "Z_PARAM_BOOL($name)"; + } + + private function parseInt(ArgInfo $argInfo): string + { + $name = $argInfo->name; + $isNull = $name.'_is_null'; + + $this->paramNames[] = "zend_long $name = 0; zend_bool $isNull = false;"; + + return $argInfo->nullable + ? "Z_PARAM_LONG_OR_NULL($name, $isNull)" + : "Z_PARAM_LONG($name)"; + } + + private function parseFloat(ArgInfo $argInfo): string + { + $name = $argInfo->name; + $isNull = $name.'_is_null'; + + $this->paramNames[] = "double $name = 0; zend_bool $isNull = false;"; + + return $argInfo->nullable + ? "Z_PARAM_DOUBLE_OR_NULL($name, $isNull)" + : "Z_PARAM_DOUBLE($name)"; + } + + private function parseArray(ArgInfo $argInfo): string + { + $name = $argInfo->name; + + $this->paramNames[] = 'zval *'.$name.' = nullptr;'; + return $argInfo->nullable + ? "Z_PARAM_ARRAY_OR_NULL($name)" + : "Z_PARAM_ARRAY($name)"; + } + + private function parseString(ArgInfo $argInfo): string + { + $name = $argInfo->name; + + $this->paramNames[] = "zend_string *".$name.' = nullptr;'; + return $argInfo->nullable + ? "Z_PARAM_STR_OR_NULL($name)" + : "Z_PARAM_STR($name)"; + } + + private function parseArgs(ArgInfo $argInfo): string + { + $name = $argInfo->name; + + $this->paramNames[] = "zval *".$name.'_argv; int '.$name.'_argc = 0;'; + return sprintf("Z_PARAM_ARRAY(*, %s, %s)", $name.'_argv', $name.'_argc'); + } + + private function parseReference(ArgInfo $argInfo): string + { + $name = $argInfo->name; + + $this->paramNames[] = "zval *".$name.' = nullptr;'; + return "Z_PARAM_ZVAL($name)".PHP_EOL; + } + + private function parseResource(ArgInfo $argInfo): string + { + $name = $argInfo->name; + + $this->paramNames[] = "zval *".$name.' = nullptr;'; + return "Z_PARAM_RESOURCE($name)".PHP_EOL; + } + + private function parseObject(ArgInfo $argInfo): string + { + $name = $argInfo->name; + + if ($argInfo->class === 'callble') { + $this->paramNames[] = "zend_fcall_info ".$name.'_fci; zend_fcall_info_cache '.$name.'_fcc;'; + if ($argInfo->nullable) { + return sprintf("Z_PARAM_FUNC_OR_NULL(%s, %s)", $name.'_fci', $name.'_fcc'); + } else { + return sprintf("Z_PARAM_FUNC(%s, %s)", $name.'_fci', $name.'_fcc'); + } + } + + $this->paramNames[] = "zval *".$name.' = nullptr;'; + return $argInfo->nullable + ? "Z_PARAM_OBJECT_OR_NULL($argInfo->name)" + : "Z_PARAM_OBJECT($argInfo->name)"; + } + + private function parseVar(ArgInfo $argInfo): string + { + $name = $argInfo->name; + if ($argInfo->typeNode instanceof NullableType) { + return match ($argInfo->typeNode->type->name) { + "string" => $this->parseString($argInfo), + "array" => $this->parseArgs($argInfo), + "bool" => $this->parseBool($argInfo), + "resource" => $this->parseResource($argInfo), + "float" => $this->parseFloat($argInfo), + default => $this->parseObject($argInfo), + }; + } else { + /** + * Union types and intersection types cannot be resolved through macros. + * Although a few macros are provided for certain union types, their coverage is limited. + * Therefore, we need to adopt a specialized approach to handle them. For now, + * we will defer this implementation until after the current PR is merged, + * so as to keep the PR size manageable and avoid making it too difficult to review. + */ + $this->complexTypes[] = $argInfo; + } + + $this->paramNames[] = "zval *".$name.' = nullptr;'; + return $argInfo->nullable + ? "Z_PARAM_ZVAL_OR_NULL($name)" + : "Z_PARAM_ZVAL($name)"; + } +} diff --git a/src/Translator.php b/src/Translator.php index 52b80885..51155591 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -34,6 +34,7 @@ use TypePhp\Exception\Skip; use TypePhp\Exception\SyntaxError; use TypePhp\Generator\DefaultArgumentGenerator; use TypePhp\Generator\LibraryImportStubGenerator; +use TypePhp\Generator\ParamParser; use TypePhp\Generator\Symbol; use TypePhp\Metadata\Constants; use TypePhp\Platform\PlatformFactory; @@ -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'); } @@ -3170,6 +3177,15 @@ CODE; 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) { @@ -3524,6 +3540,10 @@ CODE; $this->indentLevel++; $code .= $this->genScopeVarDecl(); $code .= "\n"; + + $paramParser = new ParamParser(); + $code .= $paramParser->create($this->functionDef->argInfoList); + // Runtime union/nullable parameter type checks foreach ($this->functionDef->argInfoList as $i => $argInfo) { if (!empty($argInfo->typeCheck)) { @@ -4882,4 +4902,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; + } + }