feat(python): implement native bridge for python object calls and property access

- Add callDynamicPythonMethod function for dynamic python method invocation
- Implement parsePythonObjectCall for handling python object function calls
- Add parsePythonNativeFacadeMethodCall for native facade method calls
- Implement parsePythonObjectPropertyFetch for property access translation
- Replace .call() and .attr() syntax with native php::python::callMember and getAttr
- Add native constructors mapping for python builtins like list, dict, tuple
- Update test cases to verify native bridge usage instead of zend dispatch
- Add python runtime configuration functions using native bridge
- Integrate phpx_python.h header for python native bridge support
master
韩天峰 2 weeks ago
parent aaf1ec11b3
commit a536172f82
  1. 6
      phpunit/code/python/dynamic-method.php
  2. 86
      phpunit/src/Python/PythonModuleTest.php
  3. 4
      src/Parser/FunctionCallTrait.php
  4. 19
      src/Parser/MethodCallTrait.php
  5. 5
      src/Parser/PropertyAccessTrait.php
  6. 156
      src/Python/PythonModuleTrait.php
  7. 1
      src/Translator.php

@ -0,0 +1,6 @@
<?php
function callDynamicPythonMethod(PyObject $object, string $method): mixed
{
return $object->$method(1);
}

@ -9,7 +9,39 @@ use TypePhp\Exception\TestError;
final class PythonModuleTest extends TestCase
{
public function testUsedModuleGeneratesLazyZendBindingWithoutPhpyLinkage(): void
public function testStaticallyResolvedPythonCallsUseNativeBridge(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/module-access.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
self::assertStringContainsString('php::python::callMember(', $cpp);
self::assertStringContainsString('php::python::getAttr(', $cpp);
self::assertStringContainsString('#include <phpx_python.h>', $cpp);
self::assertStringContainsString('php::python::importModule(', $extension);
self::assertStringNotContainsString('php::call(', $extension);
}
public function testDynamicPythonMethodNameStillUsesZendDispatch(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
$translator = $compiler;
$source = ROOT_PATH . '/phpunit/code/python/dynamic-method.php';
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$cpp = file_get_contents($compiler->convertFile($source));
self::assertStringContainsString('.call(', $cpp);
self::assertStringNotContainsString('php::python::callMember(', $cpp);
}
public function testUsedModuleGeneratesLazyNativeBindingWithoutPhpyLinkage(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
@ -23,13 +55,12 @@ final class PythonModuleTest extends TestCase
$extension = file_get_contents($extensionFile);
$this->assertStringContainsString('php_get_python_module(', $cpp);
$this->assertStringContainsString('.attr(', $cpp);
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('php::python::getAttr(', $cpp);
$this->assertStringContainsString('php::python::callMember(', $cpp);
$this->assertStringContainsString('THREAD_LOCAL zval php_python_module_map[1]', $extension);
$this->assertStringContainsString('php::Object php_get_python_module(', $extension);
$this->assertStringContainsString('zval_ptr_dtor(', $extension);
$this->assertStringContainsString('PyCore', $extension);
$this->assertStringContainsString('import', $extension);
$this->assertStringContainsString('php::python::importModule(', $extension);
$this->assertStringNotContainsString('#include <phpy', $extension);
$this->assertStringNotContainsString('phpy::', $extension);
$this->assertStringNotContainsString('module_that_does_not_exist', $extension);
@ -71,7 +102,7 @@ final class PythonModuleTest extends TestCase
$cpp = file_get_contents($compiler->convertFile($source));
$this->assertStringContainsString('php_get_python_module(', $cpp);
$this->assertStringContainsString('.attr(', $cpp);
$this->assertStringContainsString('php::python::getAttr(', $cpp);
}
public function testModuleStaticCallSyntaxIsRejected(): void
@ -125,8 +156,8 @@ final class PythonModuleTest extends TestCase
$extension = file_get_contents($compiler->genExtension());
$this->assertSame(4, substr_count($cpp, 'php_get_python_module('));
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('.attr(', $cpp);
$this->assertStringContainsString('php::python::callMember(', $cpp);
$this->assertStringContainsString('php::python::getAttr(', $cpp);
$this->assertStringContainsString('builtins', $extension);
$this->assertStringContainsString('math', $extension);
$this->assertStringContainsString('THREAD_LOCAL zval php_python_module_map[2]', $extension);
@ -143,8 +174,8 @@ final class PythonModuleTest extends TestCase
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('.attr(', $cpp);
$this->assertStringContainsString('php::python::callMember(', $cpp);
$this->assertStringContainsString('php::python::getAttr(', $cpp);
$this->assertStringContainsString('math', $extension);
$this->assertStringContainsString('os.path', $extension);
$this->assertStringContainsString('builtins', $extension);
@ -201,7 +232,7 @@ final class PythonModuleTest extends TestCase
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('php_get_python_module(', $cpp);
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('php::python::callMember(', $cpp);
$this->assertStringContainsString('php::Object list;', $cpp);
$this->assertStringContainsString('php::Object dict;', $cpp);
$this->assertStringContainsString('php::Object tuple;', $cpp);
@ -211,14 +242,15 @@ final class PythonModuleTest extends TestCase
$this->assertStringContainsString('php::Object object;', $cpp);
$this->assertStringContainsString('php::Object bytes;', $cpp);
$this->assertStringContainsString('scalar = php::toInt(', $cpp);
$this->assertStringContainsString('php::newObject(', $cpp);
$this->assertStringContainsString('PyList', $extension);
$this->assertStringContainsString('PyDict', $extension);
$this->assertSame(8, substr_count($cpp, 'php::python::construct('));
$this->assertStringNotContainsString('php::newObject(', $cpp);
$this->assertStringNotContainsString('php::call(php_get_persistent_class(_literal_strings', $cpp);
$this->assertStringNotContainsString('PyList', $extension);
$this->assertStringNotContainsString('PyDict', $extension);
$this->assertStringContainsString('THREAD_LOCAL zval php_python_module_map[1]', $extension);
$this->assertStringContainsString('builtins', $extension);
$this->assertStringContainsString('php_get_persistent_method(', $extension);
$this->assertStringContainsString('setOptions', $extension);
$this->assertStringContainsString('return_as_object', $extension);
$this->assertStringNotContainsString('PyCore::int', $extension);
$this->assertStringContainsString('php::python::configureRuntime(true)', $extension);
$this->assertStringContainsString('php_python_runtime_configured = false;', $extension);
$this->assertStringNotContainsString('python\\\\list', $extension);
}
@ -234,7 +266,7 @@ final class PythonModuleTest extends TestCase
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('php::python::callMember(', $cpp);
$this->assertStringContainsString('collections', $extension);
$this->assertStringContainsString('deque', $extension);
}
@ -252,7 +284,7 @@ final class PythonModuleTest extends TestCase
$this->assertStringContainsString('php_configure_python_runtime()', $cpp);
$this->assertStringContainsString('void php_configure_python_runtime()', $extension);
$this->assertStringContainsString('return_as_object', $extension);
$this->assertStringContainsString('php::python::configureRuntime(true)', $extension);
}
public function testPythonOperatorsLowerToTheOperatorModule(): void
@ -267,14 +299,14 @@ final class PythonModuleTest extends TestCase
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('operator', $extension);
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('php::python::callMember(', $cpp);
$this->assertStringContainsString('add', $extension);
$this->assertStringContainsString('iadd', $extension);
$this->assertStringContainsString('is_', $extension);
$this->assertStringContainsString('is_not', $extension);
}
public function testPythonObjectProtocolStaysOnZendDynamicDispatch(): void
public function testPythonObjectProtocolUsesNativeCallsAndZendHandlersWhereAppropriate(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
@ -285,8 +317,10 @@ final class PythonModuleTest extends TestCase
$cpp = file_get_contents($compiler->convertFile($source));
$extension = file_get_contents($compiler->genExtension());
$this->assertStringContainsString('.call(', $cpp);
$this->assertStringContainsString('.attr(', $cpp);
$this->assertStringContainsString('php::python::callMember(', $cpp);
$this->assertStringContainsString('php::python::call(', $cpp);
$this->assertStringContainsString('php::python::getAttr(', $cpp);
$this->assertStringNotContainsString('.attr(', $cpp);
$this->assertStringContainsString('.item(', $cpp);
$this->assertStringContainsString('php::ForeachIterator', $cpp);
$this->assertStringContainsString('integer = php::toInt(object);', $cpp);
@ -294,7 +328,7 @@ final class PythonModuleTest extends TestCase
$this->assertStringNotContainsString('phpy::', $cpp);
}
public function testPyObjectConversionMethodsUseThePhpyFacade(): void
public function testPyObjectConversionMethodsUseTheNativeBridge(): void
{
global $translator;
$compiler = CompilerTest::create(ROOT_PATH);
@ -308,8 +342,8 @@ final class PythonModuleTest extends TestCase
$this->assertStringContainsString('php::Array array;', $cpp);
$this->assertStringContainsString('php::Var value;', $cpp);
$this->assertStringNotContainsString('php::toArray(', $cpp);
$this->assertStringContainsString('toArray', $extension);
$this->assertStringContainsString('toValue', $extension);
$this->assertStringContainsString('php::python::toArray(', $cpp);
$this->assertStringContainsString('php::python::toValue(', $cpp);
$this->assertStringNotContainsString('php::toPlainValue(', $cpp);
$this->assertStringNotContainsString('phpy::', $cpp);
}

@ -75,6 +75,10 @@ trait FunctionCallTrait
if ($pythonCall !== null) {
return $pythonCall;
}
$pythonObjectCall = $this->parsePythonObjectCall($expr);
if ($pythonObjectCall !== null) {
return $pythonObjectCall;
}
if ($this->isVarExpr($expr->name)) {
$fn = $this->parseIdentifier($expr->name);

@ -382,6 +382,11 @@ trait MethodCallTrait
$magicMethod = false;
$method = $this->identifierToStr($expr->name, literal: true);
$pythonFacadeCall = $this->parsePythonNativeFacadeMethodCall($expr, $object);
if ($pythonFacadeCall !== null) {
return $pythonFacadeCall;
}
// Keyword methods are dispatched before all receiver-specific logic.
if ($this->isNamedMethod($expr->name)) {
$methodName = $expr->name->toString();
@ -428,6 +433,20 @@ trait MethodCallTrait
}
}
// A statically named Python member can bypass PyObject::__call and
// zend_call_function(). Variable method names remain fully dynamic and
// intentionally continue through ZendVM.
if ($this->isNamedMethod($expr->name)
&& $this->isPythonDynamicMethodCall($expr->var, $expr->name->toString())
) {
$name = $this->getLiteralString($expr->name->toString());
if ($expr->args === []) {
return 'php::python::callMember(' . $object . ', ' . $name . ')';
}
return 'php::python::callMember(' . $object . ', ' . $name . ', '
. $this->parseCallArgs($expr->args) . ')';
}
// 可转为原生调用的 MethodCall
if ($this->isVarExpr($expr->var) and $this->isNamedMethod($expr->name)) {
$type = $this->getVarType($object);

@ -918,6 +918,11 @@ trait PropertyAccessTrait
return $this->parseNullsafeExpr($expr);
}
$pythonProperty = $this->parsePythonObjectPropertyFetch($expr);
if ($pythonProperty !== null) {
return $pythonProperty;
}
$object = $expr->var;
$property = $expr->name;
$id = $this->getPropertyIdentifier($expr, $object, $property);

@ -36,6 +36,19 @@ trait PythonModuleTrait
'scalar' => true,
];
/** @var array<string, string> Builtins with a direct phpy Native ABI constructor. */
private const PYTHON_NATIVE_CONSTRUCTORS = [
'list' => 'List',
'dict' => 'Dict',
'tuple' => 'Tuple',
'set' => 'Set',
'str' => 'Str',
'object' => 'Object',
'int' => 'Int',
'float' => 'Float',
'bytes' => 'Bytes',
];
/** @var array<string, int> Case-sensitive Python module name to generated slot ID. */
protected array $pythonModuleMap = [];
@ -69,6 +82,56 @@ trait PythonModuleTrait
return $class === '' || !\TypePhp\Resolver\Reflection::hasMethod($class, $method);
}
protected function parsePythonNativeFacadeMethodCall(Expr\MethodCall $expr, string $receiver): ?string
{
if (!$this->isNamedMethod($expr->name) || !$this->isPythonObjectExpr($expr->var)) {
return null;
}
$method = $expr->name->toString();
$helper = match ($method) {
'toValue' => 'toValue',
'toArray' => 'toArray',
default => null,
};
if ($helper === null) {
return null;
}
if ($expr->args !== []) {
$this->fatalError($expr, "The {$method} method does not accept parameters");
}
$this->markPythonRuntimeUsed();
return 'php::python::' . $helper . '(' . $receiver . ')';
}
protected function parsePythonObjectCall(Expr\FuncCall $expr): ?string
{
if (!$expr->name instanceof NodeAbstract || !$this->isPythonObjectExpr($expr->name)) {
return null;
}
if ($expr->isFirstClassCallable()) {
$this->fatalError($expr, 'Python objects do not support first-class callable syntax yet');
}
$receiver = $this->addTmpVar(Type::VAR);
$this->context->beforeStmtLines[] = $receiver . ' = ' . $this->parseExpr($expr->name) . ';';
if ($expr->args === []) {
return 'php::python::call(' . $receiver . ')';
}
return 'php::python::call(' . $receiver . ', ' . $this->parseCallArgs($expr->args) . ')';
}
protected function parsePythonObjectPropertyFetch(Expr\PropertyFetch $expr): ?string
{
if ($this->isPropertyFetchUpdate($expr)
|| !$this->isIdExpr($expr->name)
|| !$this->isPythonObjectExpr($expr->var)) {
return null;
}
return 'php::python::getAttr(' . $this->parseIdentifier($expr->var) . ', '
. $this->propertyNameToStr($expr->name, literal: true) . ')';
}
protected function getPythonBinaryOperator(Expr\BinaryOp $expr): ?string
{
return match (true) {
@ -127,8 +190,8 @@ trait PythonModuleTrait
$left = $this->parseOrderedOperand($expr->left, false);
$right = $this->parseOrderedOperand($expr->right, false);
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString($this->getPythonBinaryOperator($expr))
$call = 'php::python::callMember(' . $this->getPythonModuleExpression('operator')
. ', ' . $this->getLiteralString($this->getPythonBinaryOperator($expr))
. ', php::ArgList{' . $left . ', ' . $right . '})';
return $this->pythonOperatorReturnsBool($expr)
@ -139,8 +202,7 @@ trait PythonModuleTrait
protected function convertPythonResultToBool(string $expression): string
{
$this->markPythonRuntimeUsed();
$scalar = 'php::call(' . $this->getClassEntryPtr('PyCore') . ', '
. $this->getMethodPtr('PyCore', 'scalar') . ', php::ArgList{' . $expression . '})';
$scalar = 'php::python::toValue(' . $expression . ')';
return 'php::toBool(' . $scalar . ')';
}
@ -190,8 +252,8 @@ trait PythonModuleTrait
}
$operand = $this->parseOrderedOperand($expr->expr, false);
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString($this->getPythonUnaryOperator($expr))
$call = 'php::python::callMember(' . $this->getPythonModuleExpression('operator')
. ', ' . $this->getLiteralString($this->getPythonUnaryOperator($expr))
. ', php::ArgList{' . $operand . '})';
return $expr instanceof Expr\BooleanNot ? $this->convertPythonResultToBool($call) : $call;
}
@ -201,8 +263,8 @@ trait PythonModuleTrait
if (!$this->isPythonObjectExpr($expr)) {
return null;
}
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString('truth') . ', php::ArgList{' . $parsed . '})';
$call = 'php::python::callMember(' . $this->getPythonModuleExpression('operator')
. ', ' . $this->getLiteralString('truth') . ', php::ArgList{' . $parsed . '})';
return $this->convertPythonResultToBool($call);
}
@ -251,7 +313,7 @@ trait PythonModuleTrait
} elseif ($expr->var instanceof Expr\PropertyFetch && $this->isIdExpr($expr->var->name)) {
$receiver = $this->parseOrderedOperand($expr->var->var, false);
$property = $this->propertyNameToStr($expr->var->name, literal: true);
$left = $receiver . '.attr(' . $property . ', php::AttrMode::Get)';
$left = 'php::python::getAttr(' . $receiver . ', ' . $property . ')';
$writeBack = static fn(string $value): string => 'typephp_write_property_scoped('
. $receiver . ', ' . $property . ', ' . $value . ', nullptr)';
} else {
@ -259,8 +321,8 @@ trait PythonModuleTrait
}
$right = $this->parseOrderedOperand($expr->expr, false);
$call = $this->getPythonModuleExpression('operator')
. '.call(' . $this->getLiteralString($method)
$call = 'php::python::callMember(' . $this->getPythonModuleExpression('operator')
. ', ' . $this->getLiteralString($method)
. ', php::ArgList{' . $left . ', ' . $right . '})';
if ($this->isVarExpr($expr->var)) {
return $writeBack($call);
@ -329,8 +391,6 @@ trait PythonModuleTrait
$this->pythonModuleMap[$module] = $id;
$this->markPythonRuntimeUsed();
$this->getFuncId('PyCore::import');
$this->getLiteralString('import');
$this->getLiteralString($module);
return $id;
@ -343,13 +403,6 @@ trait PythonModuleTrait
}
$this->pythonRuntimeUsed = true;
// Register runtime Zend symbols during conversion so generated map
// sizes are final before headers are emitted.
$this->getClassId('PyCore');
$this->getFuncId('PyCore::setOptions');
$this->getLiteralString('PyCore');
$this->getLiteralString('setOptions');
$this->getLiteralString('return_as_object');
}
protected function withPythonRuntimeConfigured(string $expression): string
@ -412,9 +465,10 @@ trait PythonModuleTrait
$target = $this->getPythonModuleExpression($moduleMember['module']);
$member = $this->getLiteralString($moduleMember['member']);
if ($expr->args === []) {
return $target . '.call(' . $member . ')';
return 'php::python::callMember(' . $target . ', ' . $member . ')';
}
return $target . '.call(' . $member . ', ' . $this->parseCallArgs($expr->args) . ')';
return 'php::python::callMember(' . $target . ', ' . $member . ', '
. $this->parseCallArgs($expr->args) . ')';
}
$builtin = $this->resolvePythonBuiltinName($expr->name);
@ -426,6 +480,23 @@ trait PythonModuleTrait
}
$this->markPythonRuntimeUsed();
if ($builtin === 'scalar') {
if (count($expr->args) !== 1 || $expr->args[0]->name !== null || $expr->args[0]->unpack) {
$this->fatalError($expr, 'python\\scalar() expects exactly one positional argument');
}
return 'php::python::toValue(' . $this->parseCallArgValue($expr->args[0]) . ')';
}
$nativeConstructor = self::PYTHON_NATIVE_CONSTRUCTORS[$builtin] ?? null;
if ($nativeConstructor !== null && $this->canUsePythonNativeConstructor($expr)) {
$constructor = 'php::python::Constructor::' . $nativeConstructor;
$call = $expr->args === []
? 'php::python::construct(' . $constructor . ')'
: 'php::python::construct(' . $constructor . ', '
. $this->parseCallArgValue($expr->args[0]) . ')';
return $this->withPythonRuntimeConfigured($call);
}
// This is a deliberately closed map. In particular, PyDict's PHP-array
// constructor is not equivalent to Python's dict(iterable) builtin.
$constructorClass = self::PYTHON_CONSTRUCTOR_CLASSES[$builtin] ?? null;
@ -456,9 +527,28 @@ trait PythonModuleTrait
$target = $this->getPythonModuleExpression('builtins');
$name = $this->getLiteralString($builtin);
if ($expr->args === []) {
return $target . '.call(' . $name . ')';
return 'php::python::callMember(' . $target . ', ' . $name . ')';
}
return $target . '.call(' . $name . ', ' . $this->parseCallArgs($expr->args) . ')';
return 'php::python::callMember(' . $target . ', ' . $name . ', '
. $this->parseCallArgs($expr->args) . ')';
}
/**
* The Native ABI has a deliberately small zero/one positional argument
* constructor path. Keep named and unpacked calls on Zend so its regular
* argument validation and unpacking semantics remain unchanged.
*/
private function canUsePythonNativeConstructor(Expr\FuncCall $expr): bool
{
if (count($expr->args) > 1) {
return false;
}
foreach ($expr->args as $arg) {
if ($arg->name !== null || $arg->unpack) {
return false;
}
}
return true;
}
protected function parsePythonModuleAttributeFetch(Expr\ConstFetch $expr, bool $constantExpression): ?string
@ -471,8 +561,8 @@ trait PythonModuleTrait
$this->fatalError($expr, 'Python module attributes cannot be used in constant expressions');
}
return $this->getPythonModuleExpression($moduleMember['module'])
. '.attr(' . $this->getLiteralString($moduleMember['member']) . ')';
return 'php::python::getAttr(' . $this->getPythonModuleExpression($moduleMember['module'])
. ', ' . $this->getLiteralString($moduleMember['member']) . ')';
}
protected function detectPythonExpressionReturnType(NodeAbstract $expr): ?string
@ -647,17 +737,11 @@ trait PythonModuleTrait
return '';
}
$pyCoreClass = $this->getClassEntryPtr('PyCore');
$setOptionsFunction = $this->getMethodPtr('PyCore', 'setOptions');
$returnAsObject = $this->getLiteralString('return_as_object');
$code = 'void ' . self::PREFIX . 'configure_python_runtime() {' . PHP_EOL
. 'if (EXPECTED(' . self::PREFIX . 'python_runtime_configured)) {' . PHP_EOL
. 'return;' . PHP_EOL
. '}' . PHP_EOL
. 'php::Array options;' . PHP_EOL
. 'options.set(' . $returnAsObject . ', true);' . PHP_EOL
. 'php::call(' . $pyCoreClass . ', ' . $setOptionsFunction . ', php::ArgList{options});' . PHP_EOL
. 'php::python::configureRuntime(true);' . PHP_EOL
. self::PREFIX . 'python_runtime_configured = true;' . PHP_EOL
. '}' . PHP_EOL . PHP_EOL;
@ -665,17 +749,13 @@ trait PythonModuleTrait
return $code;
}
$importFunction = $this->getMethodPtr('PyCore', 'import');
return $code
. 'php::Object ' . self::PREFIX
. 'get_python_module(int module_id, const php::Str &module_name) {' . PHP_EOL
. self::PREFIX . 'configure_python_runtime();' . PHP_EOL
. 'zval *module = &' . self::PREFIX . 'python_module_map[module_id];' . PHP_EOL
. 'if (UNEXPECTED(Z_ISUNDEF_P(module))) {' . PHP_EOL
. 'auto ce = ' . $pyCoreClass . ';' . PHP_EOL
. 'auto fn = ' . $importFunction . ';' . PHP_EOL
. 'php::Variant imported = php::call(ce, fn, php::ArgList{module_name});' . PHP_EOL
. 'php::Variant imported = php::python::importModule(module_name);' . PHP_EOL
. '(void) php::Object(imported);' . PHP_EOL
. 'imported.moveTo(module);' . PHP_EOL
. '}' . PHP_EOL

@ -88,6 +88,7 @@ class Translator extends Preprocessor
'phpx_big_int.h',
'phpx_big_float.h',
'phpx_decimal.h',
'phpx_python.h',
'typephp_helper.h',
'typephp_fiber_generator.h',
'phpx_std.h',

Loading…
Cancel
Save