类型检查

pull/44/head
NathanFreeman 3 weeks ago
parent e345b0d82a
commit 0bc91a0d6f
  1. 237
      src/Generator/ParamParser.php
  2. 37
      src/Translator.php

@ -0,0 +1,237 @@
<?php
declare(strict_types=1);
namespace TypePhp\Generator;
use PhpParser\Node\NullableType;
use TypePhp\Entity\ArgInfo;
use TypePhp\Type;
final class ParamParser
{
private const string NONE_PARAMS_TEMPLATE = <<<EOL
zend_execute_data *execute_data = EG(current_execute_data);
ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_QUIET, 0, 0)
ZEND_PARSE_PARAMETERS_END_EX(php::throwErrorIfOccurred());
EOL;
private array $paramNames = ["zend_execute_data *execute_data = EG(current_execute_data);"];
private array $complexTypes = [];
/**
* @param array<ArgInfo> $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<ArgInfo>
*/
public function getComplexTypes(): array
{
return $this->complexTypes;
}
/**
* @param array<ArgInfo> $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)";
}
}

@ -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;
}
}

Loading…
Cancel
Save