├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── phpunit.yml ├── tests ├── Stub │ ├── TestInterface.php │ ├── TestTrait.php │ ├── TestClass.php │ └── NativeNumber.php ├── bootstrap.php ├── Type │ ├── ReferenceEntryTest.php │ ├── ClosureEntryTest.php │ ├── ObjectEntryTest.php │ └── ResourceEntryTest.php ├── Reflection │ ├── ReflectionExtensionTest.php │ ├── ReflectionFunctionTest.php │ ├── ReflectionClassConstantTest.php │ ├── ReflectionValueTest.php │ └── ReflectionMethodTest.php └── System │ ├── ObjectStoreTest.php │ └── ExecutionDataTest.php ├── Dockerfile ├── preload.php ├── src ├── ClassExtension │ ├── ObjectUnsetPropertyInterface.php │ ├── ObjectHasPropertyInterface.php │ ├── ObjectCastInterface.php │ ├── ObjectReadPropertyInterface.php │ ├── ObjectDoOperationInterface.php │ ├── ObjectCompareValuesInterface.php │ ├── ObjectGetPropertyPointerInterface.php │ ├── ObjectWritePropertyInterface.php │ ├── ObjectCreateInterface.php │ ├── ObjectCreateTrait.php │ ├── ObjectGetPropertiesForInterface.php │ └── Hook │ │ ├── AbstractPropertyHook.php │ │ ├── CreateObjectHook.php │ │ ├── UnsetPropertyHook.php │ │ ├── InterfaceGetsImplementedHook.php │ │ ├── CompareValuesHook.php │ │ ├── GetPropertyPointerHook.php │ │ ├── ReadPropertyHook.php │ │ ├── HasPropertyHook.php │ │ ├── GetPropertiesForHook.php │ │ ├── WritePropertyHook.php │ │ ├── CastObjectHook.php │ │ └── DoOperationHook.php ├── EngineExtension │ ├── ControlModuleGlobalsInterface.php │ ├── ModuleDependency.php │ ├── Hook │ │ └── ExtensionConstructorHook.php │ ├── ModuleInterface.php │ └── AbstractModule.php ├── Hook │ ├── HookInterface.php │ └── AbstractHook.php ├── System │ ├── Hook │ │ └── AstProcessHook.php │ ├── Executor.php │ ├── ObjectStore.php │ └── Compiler.php ├── Type │ ├── ReferenceCountedInterface.php │ ├── ReferenceCountedTrait.php │ ├── ReferenceEntry.php │ ├── ResourceEntry.php │ ├── ClosureEntry.php │ ├── StringEntry.php │ ├── ObjectEntry.php │ ├── HashTable.php │ └── OpLine.php ├── AbstractSyntaxTree │ ├── NodeFactory.php │ ├── ListNode.php │ ├── NodeInterface.php │ ├── ValueNode.php │ ├── DeclarationNode.php │ └── Node.php ├── Reflection │ ├── ReflectionFunction.php │ ├── ReflectionClassConstant.php │ ├── ReflectionProperty.php │ ├── ReflectionExtension.php │ └── ReflectionMethod.php └── Macro │ └── DefinitionLoader.php ├── composer.json ├── LICENSE ├── phpunit.xml.dist └── CODE_OF_CONDUCT.md /.gitignore: -------------------------------------------------------------------------------- 1 | .phpunit.result.cache 2 | build/ 3 | vendor/ 4 | composer.lock 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /tests/Stub/TestInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Stub; 14 | 15 | interface TestInterface 16 | { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG PHP_VERSION 2 | FROM php:$PHP_VERSION 3 | RUN apt-get update \ 4 | && apt-get install -y libffi-dev git unzip \ 5 | && docker-php-source extract \ 6 | && docker-php-ext-install ffi \ 7 | && docker-php-source delete 8 | WORKDIR /usr/src/z-engine 9 | RUN curl -sS https://getcomposer.org/installer | php && mv ./composer.phar /usr/local/bin/composer 10 | COPY . /usr/src/z-engine 11 | RUN composer install -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | use ZEngine\Core; 14 | 15 | 16 | ini_set('display_errors', 'on'); 17 | 18 | include __DIR__ . '/../vendor/autoload.php'; 19 | 20 | Core::init(); 21 | -------------------------------------------------------------------------------- /tests/Stub/TestTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Stub; 14 | 15 | trait TestTrait 16 | { 17 | public function foo($optionalArg = null) 18 | { 19 | return $optionalArg; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /preload.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | */ 10 | declare(strict_types=1); 11 | 12 | include __DIR__.'/vendor/autoload.php'; 13 | 14 | use ZEngine\Core; 15 | 16 | /** 17 | * This file should be loaded during the preload stage, which is defined by opcache.preload file. 18 | * Either include it manually, or just add following line into your init section. 19 | */ 20 | Core::preload(); 21 | 22 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectUnsetPropertyInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\UnsetPropertyHook; 16 | 17 | /** 18 | * Interface ObjectUnsetPropertyInterface allows to intercept property unset and handle this 19 | */ 20 | interface ObjectUnsetPropertyInterface 21 | { 22 | /** 23 | * Performs reading of object's field 24 | */ 25 | public static function __fieldUnset(UnsetPropertyHook $hook): void; 26 | } 27 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectHasPropertyInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\HasPropertyHook; 16 | 17 | /** 18 | * Interface ObjectHasPropertyInterface allows to intercept property isset/has checks 19 | */ 20 | interface ObjectHasPropertyInterface 21 | { 22 | /** 23 | * Performs checking of object's field 24 | * 25 | * @return int Value to return 26 | */ 27 | public static function __fieldIsset(HasPropertyHook $hook); 28 | } 29 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectCastInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\CastObjectHook; 16 | 17 | /** 18 | * Interface ObjectCastInterface allows to cast given object to scalar values, like integer, floats, etc 19 | */ 20 | interface ObjectCastInterface 21 | { 22 | /** 23 | * Performs casting of given object to another value 24 | * 25 | * @return mixed Casted value 26 | */ 27 | public static function __cast(CastObjectHook $hook); 28 | } 29 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectReadPropertyInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\ReadPropertyHook; 16 | 17 | /** 18 | * Interface ObjectReadPropertyInterface allows to intercept property reads and modify values 19 | */ 20 | interface ObjectReadPropertyInterface 21 | { 22 | /** 23 | * Performs reading of object's field 24 | * 25 | * @return mixed Value to return 26 | */ 27 | public static function __fieldRead(ReadPropertyHook $hook); 28 | } 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lisachenko/z-engine", 3 | "description": "Library that provides direct access to native PHP structures", 4 | "type": "library", 5 | "require": { 6 | "php": ">=8.0", 7 | "ext-ffi": "*" 8 | }, 9 | "license": [ 10 | "MIT" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Alexander Lisachenko", 15 | "email": "lisachenko.it@gmail.com" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-4" : { 20 | "ZEngine\\" : "src/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4" : { 25 | "ZEngine\\" : "tests/" 26 | } 27 | }, 28 | "minimum-stability": "stable", 29 | "require-dev": { 30 | "phpunit/phpunit": "^8.5.15 || ^9.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectDoOperationInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\DoOperationHook; 16 | 17 | /** 18 | * Interface ObjectDoOperationInterface allows to perform math operations (aka operator overloading) on object 19 | */ 20 | interface ObjectDoOperationInterface 21 | { 22 | /** 23 | * Performs an operation on given object 24 | * 25 | * @return mixed Result of operation value 26 | */ 27 | public static function __doOperation(DoOperationHook $hook); 28 | } 29 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectCompareValuesInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\CompareValuesHook; 16 | 17 | /** 18 | * Interface ObjectCompareValuesInterface allows to perform comparison of objects 19 | */ 20 | interface ObjectCompareValuesInterface 21 | { 22 | /** 23 | * Performs comparison of given object with another value 24 | * 25 | * @return int Result of comparison: 1 is greater, -1 is less, 0 is equal 26 | */ 27 | public static function __compare(CompareValuesHook $hook): int; 28 | } 29 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectGetPropertyPointerInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\GetPropertyPointerHook; 16 | 17 | /** 18 | * Interface ObjectGetPropertyPointerInterface allows to intercept creation of pointers to properties (indirect changes) 19 | */ 20 | interface ObjectGetPropertyPointerInterface 21 | { 22 | /** 23 | * Returns a pointer to an object's field 24 | * 25 | * @return mixed Value to return 26 | */ 27 | public static function __fieldPointer(GetPropertyPointerHook $hook); 28 | } 29 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectWritePropertyInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\WritePropertyHook; 16 | 17 | /** 18 | * Interface ObjectWritePropertyInterface allows to intercept property writes and modify values 19 | */ 20 | interface ObjectWritePropertyInterface 21 | { 22 | /** 23 | * Performs writing of value to object's field 24 | * 25 | * @return mixed New value to write, return given $value if you don't want to adjust it 26 | */ 27 | public static function __fieldWrite(WritePropertyHook $hook); 28 | } 29 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectCreateInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use FFI\CData; 16 | use ZEngine\ClassExtension\Hook\CreateObjectHook; 17 | 18 | /** 19 | * Interface ObjectCreateInterface allows to hook into the object initialization process (eg new FooBar()) 20 | */ 21 | interface ObjectCreateInterface 22 | { 23 | /** 24 | * Performs low-level initialization of object during new instances creation 25 | * 26 | * @return CData Pointer to the zend_object instance 27 | */ 28 | public static function __init(CreateObjectHook $hook): CData; 29 | } 30 | -------------------------------------------------------------------------------- /src/EngineExtension/ControlModuleGlobalsInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\EngineExtension; 14 | 15 | use ZEngine\EngineExtension\Hook\ExtensionConstructorHook; 16 | 17 | /** 18 | * Interface ControlModuleGlobalsInterface allows to intercept module initialization/shutdown 19 | */ 20 | interface ControlModuleGlobalsInterface 21 | { 22 | /** 23 | * Callback which is called when initializing module globals 24 | * 25 | * @param ExtensionConstructorHook $hook Instance of current hook 26 | */ 27 | public static function __globalConstruct(ExtensionConstructorHook $hook): void; 28 | } 29 | -------------------------------------------------------------------------------- /src/Hook/HookInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Hook; 14 | 15 | interface HookInterface 16 | { 17 | /** 18 | * This method accepts raw C arguments for current hook and performs handling of this callback 19 | * 20 | * @param mixed ...$rawArguments 21 | */ 22 | public function handle(...$rawArguments); 23 | 24 | /** 25 | * Performs installation of current hook 26 | */ 27 | public function install(): void; 28 | 29 | /** 30 | * Checks if original handler is present to call it later with proceed 31 | */ 32 | public function hasOriginalHandler(): bool; 33 | } -------------------------------------------------------------------------------- /src/ClassExtension/ObjectCreateTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use FFI\CData; 16 | use ZEngine\ClassExtension\Hook\CreateObjectHook; 17 | 18 | /** 19 | * Trait ObjectCreateTrait contains default hook implementation for object initialization 20 | */ 21 | trait ObjectCreateTrait 22 | { 23 | /** 24 | * Performs low-level initialization of object during new instances creation 25 | * 26 | * @return CData Pointer to the zend_object instance 27 | */ 28 | public static function __init(CreateObjectHook $hook): CData 29 | { 30 | return $hook->proceed(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ClassExtension/ObjectGetPropertiesForInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension; 14 | 15 | use ZEngine\ClassExtension\Hook\GetPropertiesForHook; 16 | 17 | /** 18 | * Interface ObjectGetPropertiesForInterface allows to intercept casting to arrays, debug queries for object, etc 19 | */ 20 | interface ObjectGetPropertiesForInterface 21 | { 22 | /** 23 | * Returns a hash-map (array) representation of object (for casting to array, json encoding, var dumping) 24 | * 25 | * @return array Key-value pair of fields 26 | */ 27 | public static function __getFields(GetPropertiesForHook $hook): array; 28 | } 29 | -------------------------------------------------------------------------------- /tests/Type/ReferenceEntryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use PHPUnit\Framework\TestCase; 16 | use ZEngine\Reflection\ReflectionValue; 17 | 18 | class ReferenceEntryTest extends TestCase 19 | { 20 | public function testGetValue(): void 21 | { 22 | $value = 'some'; 23 | $reference = new ReferenceEntry($value); 24 | 25 | // At that point we will get a ReflectionValue instance of original variable 26 | $refValue = $reference->getValue(); 27 | $this->assertInstanceOf(ReflectionValue::class, $refValue); 28 | $refValue->getNativeValue($originalValue); 29 | $this->assertSame($value, $originalValue); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Lisachenko Alexander 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/Stub/TestClass.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Stub; 14 | 15 | class TestClass 16 | { 17 | public const SOME_CONST = 123; 18 | 19 | public int $property = 42; 20 | 21 | private int $secret = 100500; 22 | 23 | /** 24 | * This method will be removed during the test, do not call it or use it 25 | */ 26 | private function methodToRemove(): void 27 | { 28 | die('Method should not be called and must be removed'); 29 | } 30 | 31 | public function reflectedMethod(): ?string 32 | { 33 | // If we make this method static in runtime, then $this won't be passed to it 34 | return isset($this) ? get_class($this) : null; 35 | } 36 | 37 | public function setSecret(int $newSecret): void 38 | { 39 | $this->secret = $newSecret; 40 | } 41 | 42 | public function tellSecret(): int 43 | { 44 | return $this->secret; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 11 | 15 | 16 | 17 | ./src/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ./tests/ 28 | 29 | 30 | 31 | 32 | performance 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/EngineExtension/ModuleDependency.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\EngineExtension; 14 | 15 | 16 | use FFI\CData; 17 | 18 | /** 19 | * Class ModuleDependency 20 | * 21 | * struct _zend_module_dep { 22 | * const char *name; // module name 23 | * const char *rel; // version relationship: NULL (exists), lt|le|eq|ge|gt (to given version) 24 | * const char *version; // version 25 | * unsigned char type; // dependency type 26 | * }; 27 | */ 28 | class ModuleDependency 29 | { 30 | public const MODULE_REQUIRED = 1; 31 | public const MODULE_CONFLICTS = 2; 32 | public const MODULE_OPTIONAL = 3; 33 | 34 | /** 35 | * Holds a _zend_module_dep structure 36 | */ 37 | private CData $entry; 38 | 39 | public function __construct( 40 | string $name, 41 | int $relationType, 42 | string $version, 43 | int $dependencyType = self::MODULE_REQUIRED 44 | ) { 45 | 46 | $this->name = $name; 47 | $this->relationType = $relationType; 48 | $this->version = $version; 49 | $this->dependencyType = $dependencyType; 50 | } 51 | } -------------------------------------------------------------------------------- /src/ClassExtension/Hook/AbstractPropertyHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Hook\AbstractHook; 17 | use ZEngine\Type\ObjectEntry; 18 | use ZEngine\Type\StringEntry; 19 | 20 | /** 21 | * Abstract object property operational hook 22 | */ 23 | abstract class AbstractPropertyHook extends AbstractHook 24 | { 25 | /** 26 | * Object instance 27 | */ 28 | protected CData $object; 29 | 30 | /** 31 | * Member name 32 | */ 33 | protected CData $member; 34 | 35 | /** 36 | * Internal cache slot (for native callback only) 37 | */ 38 | protected ?CData $cacheSlot; 39 | 40 | /** 41 | * Returns an object instance 42 | */ 43 | public function getObject(): object 44 | { 45 | $objectInstance = ObjectEntry::fromCData($this->object)->getNativeValue(); 46 | 47 | return $objectInstance; 48 | } 49 | 50 | /** 51 | * Returns a member name 52 | */ 53 | public function getMemberName(): string 54 | { 55 | $memberName = StringEntry::fromCData($this->member)->getStringValue(); 56 | 57 | return $memberName; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/EngineExtension/Hook/ExtensionConstructorHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\EngineExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Hook\AbstractHook; 17 | 18 | /** 19 | * Receiving hook for extension global memory constructor 20 | */ 21 | class ExtensionConstructorHook extends AbstractHook 22 | { 23 | protected const HOOK_FIELD = 'globals_ctor'; 24 | 25 | private CData $globalMemoryPointer; 26 | 27 | /** 28 | * Returns a raw memory pointer 29 | */ 30 | public function getMemoryPointer(): CData 31 | { 32 | return $this->globalMemoryPointer; 33 | } 34 | 35 | /** 36 | * void (*globals_ctor)(void *global); 37 | * 38 | * @inheritDoc 39 | */ 40 | public function handle(...$rawArguments): void 41 | { 42 | [$this->globalMemoryPointer] = $rawArguments; 43 | 44 | ($this->userHandler)($this); 45 | } 46 | 47 | /** 48 | * Proceeds with default implementation (if present) 49 | */ 50 | public function proceed(): void 51 | { 52 | if ($this->originalHandler !== null) { 53 | ($this->originalHandler)($this->globalMemoryPointer); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/System/Hook/AstProcessHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\System\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\AbstractSyntaxTree\NodeFactory; 17 | use ZEngine\AbstractSyntaxTree\NodeInterface; 18 | use ZEngine\Hook\AbstractHook; 19 | 20 | /** 21 | * Receiving hook for processing an AST 22 | */ 23 | class AstProcessHook extends AbstractHook 24 | { 25 | protected const HOOK_FIELD = 'zend_ast_process'; 26 | 27 | /** 28 | * Instance of top-level AST node 29 | */ 30 | protected CData $ast; 31 | 32 | /** 33 | * typedef void (*zend_ast_process_t)(zend_ast *ast); 34 | * 35 | * @inheritDoc 36 | */ 37 | public function handle(...$rawArguments): void 38 | { 39 | [$this->ast] = $rawArguments; 40 | 41 | ($this->userHandler)($this); 42 | } 43 | 44 | /** 45 | * Returns a top-level node element 46 | */ 47 | public function getAST(): NodeInterface 48 | { 49 | return NodeFactory::fromCData($this->ast); 50 | } 51 | 52 | /** 53 | * Proceeds with default callback 54 | */ 55 | public function proceed() 56 | { 57 | if (!$this->hasOriginalHandler()) { 58 | throw new \LogicException('Original handler is not available'); 59 | } 60 | ($this->originalHandler)($this->ast); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Type/ReferenceCountedInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | /** 16 | * Interface for all refcounted entries 17 | */ 18 | interface ReferenceCountedInterface 19 | { 20 | public const GC_COLLECTABLE = (1 << 4); 21 | public const GC_PROTECTED = (1 << 5); // used for recursion detection 22 | public const GC_IMMUTABLE = (1 << 6); // can't be canged in place 23 | public const GC_PERSISTENT = (1 << 7); // allocated using malloc 24 | public const GC_PERSISTENT_LOCAL = (1 << 8); // persistent, but thread-local 25 | 26 | /** 27 | * Returns an internal reference counter value 28 | */ 29 | public function getReferenceCount(): int; 30 | 31 | /** 32 | * Increments a reference counter, so this object will live more than current scope 33 | */ 34 | public function incrementReferenceCount(): int; 35 | 36 | /** 37 | * Decrements a reference counter 38 | */ 39 | public function decrementReferenceCount(): int; 40 | 41 | /** 42 | * Checks if this variable is immutable or not 43 | */ 44 | public function isImmutable(): bool; 45 | 46 | /** 47 | * Checks if this variable is persistent (allocated using malloc) 48 | */ 49 | public function isPersistent(): bool; 50 | 51 | /** 52 | * Checks if this variable is persistent for thread via thread-local-storage (TLS) 53 | */ 54 | public function isPersistentLocal(): bool; 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: "PHPUnit tests" 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | phpunit: 8 | name: "PHPUnit tests" 9 | 10 | runs-on: ${{ matrix.operating-system }} 11 | 12 | strategy: 13 | matrix: 14 | dependencies: 15 | - "lowest" 16 | - "highest" 17 | php-version: 18 | - "8.0" 19 | operating-system: 20 | - "ubuntu-latest" 21 | 22 | steps: 23 | - name: "Checkout" 24 | uses: "actions/checkout@v2" 25 | 26 | - name: "Install PHP" 27 | uses: "shivammathur/setup-php@v2" 28 | with: 29 | coverage: "xdebug" 30 | php-version: "${{ matrix.php-version }}" 31 | ini-values: memory_limit=-1 32 | tools: composer:v2, cs2pr 33 | 34 | - name: "Cache dependencies" 35 | uses: "actions/cache@v2" 36 | with: 37 | path: | 38 | ~/.composer/cache 39 | vendor 40 | key: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 41 | restore-keys: "php-${{ matrix.php-version }}-${{ matrix.dependencies }}" 42 | 43 | - name: "Install lowest dependencies" 44 | if: ${{ matrix.dependencies == 'lowest' }} 45 | run: "composer update --prefer-lowest --no-interaction --no-progress --no-suggest" 46 | 47 | - name: "Install highest dependencies" 48 | if: ${{ matrix.dependencies == 'highest' }} 49 | run: "composer update --no-interaction --no-progress --no-suggest" 50 | 51 | - name: "Install locked dependencies" 52 | if: ${{ matrix.dependencies == 'locked' }} 53 | run: "composer install --no-interaction --no-progress --no-suggest" 54 | 55 | - name: "Tests" 56 | run: "XDEBUG_MODE=coverage vendor/bin/phpunit" 57 | -------------------------------------------------------------------------------- /src/AbstractSyntaxTree/NodeFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\AbstractSyntaxTree; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | 18 | /** 19 | * Node factory is used to create an instance of concrete Node class from raw CData `zend_ast` entry 20 | */ 21 | class NodeFactory 22 | { 23 | /** 24 | * Factory method that creates an instance of PHP node from C representation 25 | * 26 | * @param CData $node 27 | * 28 | * @return NodeInterface 29 | */ 30 | public static function fromCData(CData $node): NodeInterface 31 | { 32 | $kind = $node->kind; 33 | switch (true) { 34 | // There are special node types ZVAL, CONSTANT, ZNODE 35 | case $kind === NodeKind::AST_ZVAL: 36 | $node = Core::cast('zend_ast_zval *', $node); 37 | return ValueNode::fromCData($node); 38 | case $kind === NodeKind::AST_CONSTANT: 39 | case $kind === NodeKind::AST_ZNODE: 40 | throw new \RuntimeException('Not yet supported: ' . NodeKind::name($kind)); 41 | case NodeKind::isSpecial($kind): 42 | $node = Core::cast('zend_ast_decl *', $node); 43 | return DeclarationNode::fromCData($node); 44 | case NodeKind::isList($kind): 45 | $node = Core::cast('zend_ast_list *', $node); 46 | return ListNode::fromCData($node); 47 | default: 48 | return Node::fromCData($node); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/CreateObjectHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Hook\AbstractHook; 17 | use ZEngine\Reflection\ReflectionClass; 18 | 19 | /** 20 | * Receiving hook for performing operation on object 21 | */ 22 | class CreateObjectHook extends AbstractHook 23 | { 24 | protected const HOOK_FIELD = 'create_object'; 25 | 26 | private CData $classType; 27 | 28 | /** 29 | * Returns a raw class type (zend_class_entry) 30 | */ 31 | public function getClassType(): CData 32 | { 33 | return $this->classType; 34 | } 35 | 36 | /** 37 | * Changes a class type to create 38 | */ 39 | public function setClassType(CData $classType): void 40 | { 41 | $this->classType = $classType; 42 | } 43 | 44 | /** 45 | * zend_object* (*create_object)(zend_class_entry *class_type); 46 | * 47 | * @inheritDoc 48 | */ 49 | public function handle(...$rawArguments): CData 50 | { 51 | [$this->classType] = $rawArguments; 52 | 53 | return ($this->userHandler)($this); 54 | } 55 | 56 | /** 57 | * Proceeds with object creation 58 | */ 59 | public function proceed() 60 | { 61 | if ($this->originalHandler === null) { 62 | $object = ReflectionClass::newInstanceRaw($this->classType); 63 | } else { 64 | $object = ($this->originalHandler)($this->classType); 65 | } 66 | 67 | return $object; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/UnsetPropertyHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Hook\AbstractHook; 18 | use ZEngine\Reflection\ReflectionValue; 19 | 20 | /** 21 | * Receiving hook for object field unset operation 22 | */ 23 | class UnsetPropertyHook extends AbstractPropertyHook 24 | { 25 | protected const HOOK_FIELD = 'unset_property'; 26 | 27 | /** 28 | * typedef void (*zend_object_unset_property_t)(zend_object *object, zend_string *member, void **cache_slot); 29 | * 30 | * @inheritDoc 31 | */ 32 | public function handle(...$rawArguments): void 33 | { 34 | [$this->object, $this->member, $this->cacheSlot] = $rawArguments; 35 | 36 | ($this->userHandler)($this); 37 | } 38 | 39 | /** 40 | * Proceeds with default handler 41 | */ 42 | public function proceed() 43 | { 44 | if (!$this->hasOriginalHandler()) { 45 | throw new \LogicException('Original handler is not available'); 46 | } 47 | 48 | // As we will play with EG(fake_scope), we won't be able to access private or protected members, need to unpack 49 | $originalHandler = $this->originalHandler; 50 | 51 | $object = $this->object; 52 | $member = $this->member; 53 | $cacheSlot = $this->cacheSlot; 54 | 55 | $previousScope = Core::$executor->setFakeScope(Core::$executor->getExecutionState()->getThis()->getRawObject()->ce); 56 | ($originalHandler)($object, $member, $cacheSlot); 57 | Core::$executor->setFakeScope($previousScope); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/InterfaceGetsImplementedHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Hook\AbstractHook; 17 | use ZEngine\Reflection\ReflectionClass; 18 | 19 | /** 20 | * Receiving hook for interface implementation 21 | */ 22 | class InterfaceGetsImplementedHook extends AbstractHook 23 | { 24 | protected const HOOK_FIELD = 'interface_gets_implemented'; 25 | 26 | /** 27 | * Interface type that is implemented 28 | */ 29 | protected CData $interfaceType; 30 | 31 | /** 32 | * Class that implements interface 33 | */ 34 | protected CData $classType; 35 | 36 | /** 37 | * int (*interface_gets_implemented)(zend_class_entry *iface, zend_class_entry *class_type); 38 | * 39 | * @inheritDoc 40 | */ 41 | public function handle(...$rawArguments): int 42 | { 43 | [$this->interfaceType, $this->classType] = $rawArguments; 44 | 45 | $result = ($this->userHandler)($this); 46 | 47 | return $result; 48 | } 49 | 50 | /** 51 | * Returns a class that implements interface 52 | */ 53 | public function getClass(): ReflectionClass 54 | { 55 | return ReflectionClass::fromCData($this->classType); 56 | } 57 | 58 | /** 59 | * Proceeds with default handler 60 | */ 61 | public function proceed() 62 | { 63 | if (!$this->hasOriginalHandler()) { 64 | throw new \LogicException('Original handler is not available'); 65 | } 66 | 67 | $result = ($this->originalHandler)($this->interfaceType, $this->classType); 68 | 69 | return $result; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/EngineExtension/ModuleInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\EngineExtension; 14 | 15 | /** 16 | * Declares general ModuleInterface which is used for declaration of userland PHP extensions 17 | */ 18 | interface ModuleInterface 19 | { 20 | /** 21 | * Returns the target debug mode for this module 22 | * 23 | * Use ZEND_DEBUG_BUILD as default if your module does not depend on debug mode. 24 | */ 25 | public static function targetDebug(): bool; 26 | 27 | /** 28 | * Returns the target API version for this module 29 | * 30 | * @see zend_modules.h:ZEND_MODULE_API_NO 31 | */ 32 | public static function targetApiVersion(): int; 33 | 34 | /** 35 | * Returns true if this module should be persistent or false if temporary 36 | */ 37 | public static function targetPersistent(): bool; 38 | 39 | /** 40 | * Returns the target thread-safe mode for this module 41 | * 42 | * Use ZEND_THREAD_SAFE as default if your module does not depend on thread-safe mode. 43 | */ 44 | public static function targetThreadSafe(): bool; 45 | 46 | /** 47 | * Returns global type (if present) or null if module doesn't use global memory 48 | */ 49 | public static function globalType(): ?string; 50 | 51 | /** 52 | * Starts this module 53 | * 54 | * Startup includes calling callbacks for global memory allocation, checking deps, etc 55 | */ 56 | public function startup(): void; 57 | 58 | /** 59 | * Checks if this module loaded or not 60 | */ 61 | public function isModuleRegistered(): bool; 62 | 63 | /** 64 | * Performs registration of this module in the engine 65 | */ 66 | public function register(): void; 67 | } -------------------------------------------------------------------------------- /tests/Reflection/ReflectionExtensionTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class ReflectionExtensionTest extends TestCase 18 | { 19 | private ReflectionExtension $refExtension; 20 | 21 | protected function setUp(): void 22 | { 23 | // As FFI is always required for this framework, we can be sure that it is present 24 | $this->refExtension = new ReflectionExtension('ffi'); 25 | } 26 | 27 | public function testReturnsThreadSafe(): void 28 | { 29 | $this->assertSame(ZEND_THREAD_SAFE, $this->refExtension->isThreadSafe()); 30 | } 31 | 32 | public function testReturnsDebug(): void 33 | { 34 | $this->assertSame(ZEND_DEBUG_BUILD, $this->refExtension->isDebug()); 35 | } 36 | 37 | public function testModuleWasStarted(): void 38 | { 39 | // Built-in modules always started, only our custom modules may be in non-started state 40 | $this->assertSame(true, $this->refExtension->wasModuleStarted()); 41 | } 42 | 43 | public function testReturnsModuleNumber(): void 44 | { 45 | // each module has it's own unique module number greater than zero 46 | $this->assertGreaterThan(0,$this->refExtension->getModuleNumber()); 47 | } 48 | 49 | public function testGetGlobals(): void 50 | { 51 | /* @see https://github.com/php/php-src/blob/PHP-7.4/ext/ffi/php_ffi.h#L33-L63 */ 52 | $this->assertNotNull($this->refExtension->getGlobals()); 53 | } 54 | 55 | public function testGetGlobalsSize(): void 56 | { 57 | /* @see https://github.com/php/php-src/blob/PHP-7.4/ext/ffi/php_ffi.h#L33-L63 */ 58 | $this->assertGreaterThan(0, $this->refExtension->getGlobalsSize()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Type/ClosureEntryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use Closure; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class ClosureEntryTest extends TestCase 19 | { 20 | private Closure $closure; 21 | 22 | protected function setUp(): void 23 | { 24 | $this->closure = function () { 25 | $self = isset($this) ? $this : null; 26 | 27 | return [ 28 | 'class' => $self ? get_class($self) : null, 29 | 'scope' => get_called_class() 30 | ]; 31 | }; 32 | } 33 | 34 | public function testGetCalledScope(): void 35 | { 36 | $scope = (new ClosureEntry($this->closure))->getCalledScope(); 37 | $this->assertSame(self::class, $scope); 38 | $result = ($this->closure)(); 39 | $this->assertSame(get_class($this), $result['scope']); 40 | } 41 | 42 | /** 43 | * @group internal 44 | */ 45 | public function testSetCalledScope(): void 46 | { 47 | $closureEntry = new ClosureEntry($this->closure); 48 | $closureEntry->setCalledScope(\Exception::class); 49 | 50 | $scope = $closureEntry->getCalledScope(); 51 | 52 | $this->assertSame(\Exception::class, $scope); 53 | $result = ($this->closure)(); 54 | $this->markTestIncomplete('This test does not update internal scope variable, or it is cached'); 55 | } 56 | 57 | /** 58 | * @group internal 59 | */ 60 | public function testSetThis(): void 61 | { 62 | $closureEntry = new ClosureEntry($this->closure); 63 | $closureEntry->setThis(new \Exception()); 64 | 65 | $result = ($this->closure)(); 66 | 67 | $this->assertSame(\Exception::class, $result['scope']); 68 | $this->assertSame(\Exception::class, $result['class']); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/CompareValuesHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Hook\AbstractHook; 17 | use ZEngine\Reflection\ReflectionValue; 18 | 19 | /** 20 | * Receiving hook for performing operation on object 21 | */ 22 | class CompareValuesHook extends AbstractHook 23 | { 24 | protected const HOOK_FIELD = 'compare'; 25 | 26 | /** 27 | * Holds a return value 28 | */ 29 | protected CData $returnValue; 30 | 31 | /** 32 | * First operand 33 | */ 34 | protected CData $op1; 35 | 36 | /** 37 | * Second operand 38 | */ 39 | protected CData $op2; 40 | 41 | /** 42 | * typedef int (*zend_object_compare_t)(zval *object1, zval *object2); 43 | * 44 | * @inheritDoc 45 | */ 46 | public function handle(...$rawArguments): int 47 | { 48 | [$this->op1, $this->op2] = $rawArguments; 49 | 50 | $result = ($this->userHandler)($this); 51 | 52 | return $result; 53 | } 54 | 55 | /** 56 | * Returns first operand 57 | */ 58 | public function getFirst() 59 | { 60 | ReflectionValue::fromValueEntry($this->op1)->getNativeValue($value); 61 | 62 | return $value; 63 | } 64 | 65 | /** 66 | * Returns second operand 67 | */ 68 | public function getSecond() 69 | { 70 | ReflectionValue::fromValueEntry($this->op2)->getNativeValue($value); 71 | 72 | return $value; 73 | } 74 | 75 | /** 76 | * Proceeds with object comparison 77 | */ 78 | public function proceed() 79 | { 80 | if (!$this->hasOriginalHandler()) { 81 | throw new \LogicException('Original handler is not available'); 82 | } 83 | $result = ($this->originalHandler)($this->op1, $this->op2); 84 | 85 | return $result; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Type/ReferenceCountedTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | 17 | /** 18 | * Trait RefcountedTrait 19 | */ 20 | trait ReferenceCountedTrait 21 | { 22 | /** 23 | * Returns an internal reference counter value 24 | */ 25 | public function getReferenceCount(): int 26 | { 27 | return $this->getGC()->refcount; 28 | } 29 | 30 | /** 31 | * Increments a reference counter, so this object will live more than current scope 32 | * 33 | * @see zend_types.h:zend_gc_addref(zend_refcounted_h *p) 34 | */ 35 | public function incrementReferenceCount(): int 36 | { 37 | return ++$this->getGC()->refcount; 38 | } 39 | 40 | /** 41 | * Decrements a reference counter 42 | * 43 | * @see zend_types.h:zend_gc_delref(zend_refcounted_h *p) 44 | */ 45 | public function decrementReferenceCount(): int 46 | { 47 | assert($this->getGC()->refcount > 0); 48 | 49 | return --$this->getGC()->refcount; 50 | } 51 | 52 | /** 53 | * Checks if this variable is immutable or not 54 | */ 55 | public function isImmutable(): bool 56 | { 57 | return (bool) ($this->getGC()->u->type_info & ReferenceCountedInterface::GC_IMMUTABLE); 58 | } 59 | 60 | /** 61 | * Checks if this variable is persistent (allocated using malloc) 62 | */ 63 | public function isPersistent(): bool 64 | { 65 | return (bool) ($this->getGC()->u->type_info & ReferenceCountedInterface::GC_PERSISTENT); 66 | } 67 | 68 | /** 69 | * Checks if this variable is persistent for thread via thread-local-storage (TLS) 70 | */ 71 | public function isPersistentLocal(): bool 72 | { 73 | return (bool) ($this->getGC()->u->type_info & ReferenceCountedInterface::GC_PERSISTENT_LOCAL); 74 | } 75 | 76 | /** 77 | * This method should return an instance of zend_refcounted_h 78 | */ 79 | abstract protected function getGC(): CData; 80 | } 81 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/GetPropertyPointerHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Reflection\ReflectionValue; 18 | 19 | /** 20 | * Receiving hook for indirect property access (by reference or via $this->field++) 21 | */ 22 | class GetPropertyPointerHook extends AbstractPropertyHook 23 | { 24 | protected const HOOK_FIELD = 'get_property_ptr_ptr'; 25 | 26 | /** 27 | * Hook access type 28 | */ 29 | protected int $type; 30 | 31 | /** 32 | * typedef zval *(*zend_object_get_property_ptr_ptr_t)(zend_object *object, zend_string *member, int type, void **cache_slot) 33 | * 34 | * @inheritDoc 35 | */ 36 | public function handle(...$rawArguments) 37 | { 38 | [$this->object, $this->member, $this->type, $this->cacheSlot] = $rawArguments; 39 | 40 | $result = ($this->userHandler)($this); 41 | 42 | return $result; 43 | } 44 | 45 | /** 46 | * Returns the access type 47 | */ 48 | public function getAccessType(): int 49 | { 50 | return $this->type; 51 | } 52 | 53 | /** 54 | * Proceeds with default handler 55 | */ 56 | public function proceed() 57 | { 58 | if (!$this->hasOriginalHandler()) { 59 | throw new \LogicException('Original handler is not available'); 60 | } 61 | 62 | // As we will play with EG(fake_scope), we won't be able to access private or protected members, need to unpack 63 | $originalHandler = $this->originalHandler; 64 | 65 | $object = $this->object; 66 | $member = $this->member; 67 | $type = $this->type; 68 | $cacheSlot = $this->cacheSlot; 69 | 70 | $previousScope = Core::$executor->setFakeScope($object->ce); 71 | $result = ($originalHandler)($object, $member, $type, $cacheSlot); 72 | Core::$executor->setFakeScope($previousScope); 73 | 74 | return $result; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Type/ObjectEntryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use PHPUnit\Framework\TestCase; 16 | 17 | class ObjectEntryTest extends TestCase 18 | { 19 | private object $instance; 20 | 21 | protected function setUp(): void 22 | { 23 | $this->instance = new \RuntimeException('Test'); 24 | } 25 | 26 | public function testGetClass(): void 27 | { 28 | $class = (new ObjectEntry($this->instance))->getClass(); 29 | $this->assertSame(\RuntimeException::class, $class->getName()); 30 | } 31 | 32 | /** 33 | * @group internal 34 | */ 35 | public function testSetClass(): void 36 | { 37 | $objectEntry = new ObjectEntry($this->instance); 38 | $objectEntry->setClass(\Exception::class); 39 | 40 | $className = get_class($this->instance); 41 | 42 | $this->assertSame(\Exception::class, $className); 43 | } 44 | 45 | public function testGetHandle(): void 46 | { 47 | $objectEntry = new ObjectEntry($this->instance); 48 | $objectHandle = spl_object_id($this->instance); 49 | 50 | $this->assertSame($objectHandle, $objectEntry->getHandle()); 51 | } 52 | 53 | /** 54 | * @depends testGetHandle 55 | * @group internal 56 | */ 57 | public function testSetHandle(): void 58 | { 59 | $objectEntry = new ObjectEntry($this->instance); 60 | $originalHandle = spl_object_id($this->instance); 61 | $entryHandle = spl_object_id($objectEntry); 62 | // We just update a handle for internal object to be the same as $objectEntry itself 63 | $objectEntry->setHandle($entryHandle); 64 | 65 | $this->assertSame(spl_object_id($objectEntry), spl_object_id($this->instance)); 66 | $this->assertNotSame($objectEntry, $this->instance); 67 | 68 | // This is required to prevent a ZEND_ASSERT(EG(objects_store).object_buckets != NULL) during shutdown 69 | $objectEntry->setHandle($originalHandle); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AbstractSyntaxTree/ListNode.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\AbstractSyntaxTree; 14 | 15 | use ZEngine\Core; 16 | 17 | /** 18 | * List node is used where the number of children is determined dynamically. 19 | * 20 | * It is identical to ordinary AST nodes, but contains an additional children count. 21 | * 22 | * typedef struct _zend_ast_list { 23 | * zend_ast_kind kind; 24 | * zend_ast_attr attr; 25 | * zend_uint lineno; 26 | * zend_uint children; 27 | * zend_ast *child[1]; 28 | * } zend_ast_list; 29 | */ 30 | class ListNode extends Node 31 | { 32 | /** 33 | * Creates a list of given type 34 | * 35 | * @param int $kind 36 | */ 37 | public function __construct(int $kind) 38 | { 39 | if (!NodeKind::isList($kind)) { 40 | $kindName = NodeKind::name($kind); 41 | throw new \InvalidArgumentException('Given AST type ' . $kindName . ' does not belong to list type'); 42 | } 43 | 44 | $ast = Core::call('zend_ast_create_list_0', $kind); 45 | $list = Core::cast('zend_ast_list *', $ast); 46 | 47 | $this->node = $list; 48 | } 49 | 50 | /** 51 | * Returns children node count 52 | */ 53 | public function getChildrenCount(): int 54 | { 55 | // List stores the number of nodes in separate field 56 | return $this->node->children; 57 | } 58 | 59 | /** 60 | * Adds one or several nodes to the list 61 | * 62 | * @param NodeInterface ...$nodes List of nodes to add 63 | */ 64 | public function append(NodeInterface ...$nodes): void 65 | { 66 | // This variable can be redeclared (if list will grow during node addition) 67 | $selfNode = Core::cast('zend_ast *', $this->node); 68 | foreach ($nodes as $node) { 69 | $astNode = Core::cast('zend_ast *', $node->node); 70 | $selfNode = Core::call('zend_ast_list_add', $selfNode, $astNode); 71 | } 72 | 73 | $this->node = Core::cast('zend_ast_list *', $selfNode); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/System/Executor.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\System; 14 | 15 | use FFI\CData; 16 | use ZEngine\Reflection\ReflectionValue; 17 | use ZEngine\Type\HashTable; 18 | use ZEngine\Type\ObjectEntry; 19 | 20 | class Executor 21 | { 22 | /** 23 | * Contains a hashtable with all registered classes 24 | * 25 | * @var HashTable|ReflectionValue[string] 26 | */ 27 | public HashTable $classTable; 28 | 29 | /** 30 | * Contains a hashtable with all registered functions 31 | * 32 | * @var HashTable|ReflectionValue[] 33 | */ 34 | public HashTable $functionTable; 35 | 36 | /** 37 | * Represents the global object storage 38 | * 39 | * @var ObjectStore|ObjectEntry[] 40 | */ 41 | public ObjectStore $objectStore; 42 | 43 | /** 44 | * Holds an internal pointer to the executor_globals structure 45 | */ 46 | private CData $pointer; 47 | 48 | public function __construct(CData $pointer) 49 | { 50 | $this->pointer = $pointer; 51 | $this->classTable = new HashTable($pointer->class_table); 52 | $this->functionTable = new HashTable($pointer->function_table); 53 | $this->objectStore = new ObjectStore($pointer->objects_store); 54 | } 55 | 56 | /** 57 | * Returns an execution state with scope, variables, etc. 58 | */ 59 | public function getExecutionState(): ExecutionData 60 | { 61 | // current_execute_data refers to the getExecutionState itself, so we move to the previous item 62 | $executionState = new ExecutionData($this->pointer->current_execute_data->prev_execute_data); 63 | 64 | return $executionState; 65 | } 66 | 67 | /** 68 | * Set a new fake scope and returns previous value (to restore it later) 69 | * 70 | * @return CData|null 71 | */ 72 | public function setFakeScope(?CData $newScope): ?CData 73 | { 74 | $oldScope = $this->pointer->fake_scope; 75 | $this->pointer->fake_scope = $newScope; 76 | 77 | return $oldScope; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/AbstractSyntaxTree/NodeInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\AbstractSyntaxTree; 14 | 15 | 16 | /** 17 | * General AST node interface 18 | */ 19 | interface NodeInterface 20 | { 21 | /** 22 | * Returns the constant indicating the type of the AST node 23 | * 24 | * @see NodeKind class constants 25 | */ 26 | public function getKind(): int; 27 | 28 | /** 29 | * Returns node's kind-specific flags 30 | */ 31 | public function getAttributes(): int; 32 | 33 | /** 34 | * Changes node attributes 35 | */ 36 | public function setAttributes(int $newAttributes): int; 37 | 38 | /** 39 | * Returns the start line number of the node 40 | */ 41 | public function getLine(): int; 42 | 43 | /** 44 | * Changes the node line 45 | */ 46 | public function setLine(int $newLine): void; 47 | 48 | /** 49 | * Returns the number of children for this node 50 | */ 51 | public function getChildrenCount(): int; 52 | 53 | /** 54 | * Returns children of this node 55 | * 56 | * @return NodeInterface[] 57 | */ 58 | public function getChildren(): array; 59 | 60 | /** 61 | * Dumps current node in friendly format 62 | * 63 | * @param int $indent Level of indentation 64 | */ 65 | public function dump(int $indent = 0): string; 66 | 67 | /** 68 | * Replace one child node with another one without checks 69 | * 70 | * @param int $index Child node index 71 | * @param NodeInterface $node New node to use 72 | */ 73 | public function replaceChild(int $index, NodeInterface $node): void; 74 | 75 | /** 76 | * Return concrete child by index (can be empty) 77 | * 78 | * @param int $index Index of child node 79 | */ 80 | public function getChild(int $index): ?NodeInterface; 81 | 82 | /** 83 | * Removes a child node from the tree and returns the removed node. 84 | * 85 | * @param int $index Index of the node to remove 86 | */ 87 | public function removeChild(int $index): NodeInterface; 88 | } 89 | -------------------------------------------------------------------------------- /tests/Type/ResourceEntryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class ResourceEntryTest extends TestCase 19 | { 20 | private $file; 21 | 22 | protected function setUp(): void 23 | { 24 | $this->file = fopen(__FILE__, 'r'); 25 | } 26 | 27 | protected function tearDown(): void 28 | { 29 | fclose($this->file); 30 | } 31 | 32 | public function testGetHandle(): void 33 | { 34 | $refResource = new ResourceEntry($this->file); 35 | 36 | preg_match('/Resource id #(\d+)/', (string)$this->file, $matches); 37 | $this->assertSame((int)$matches[1], $refResource->getHandle()); 38 | $refResource->setHandle(1); 39 | $this->assertSame(1, $refResource->getHandle()); 40 | } 41 | 42 | /** 43 | * @group internal 44 | */ 45 | public function testSetHandle(): void 46 | { 47 | $refResource = new ResourceEntry($this->file); 48 | 49 | $refResource->setHandle(1); 50 | $this->assertSame(1, $refResource->getHandle()); 51 | } 52 | 53 | public function testGetRawData() 54 | { 55 | $refResource = new ResourceEntry($this->file); 56 | $rawData = $refResource->getRawData(); 57 | $this->assertInstanceOf(CData::class, $rawData); 58 | } 59 | 60 | public function testGetType() 61 | { 62 | $refResource = new ResourceEntry($this->file); 63 | 64 | // stream resource type has an id=2 65 | $this->assertSame(2, $refResource->getType()); 66 | } 67 | 68 | /** 69 | * @group internal 70 | */ 71 | public function testSetType() 72 | { 73 | $refResource = new ResourceEntry($this->file); 74 | 75 | // persistent_stream has type=3 76 | $refResource->setType(3); 77 | $this->assertSame(3, $refResource->getType()); 78 | ob_start(); 79 | var_dump($this->file); 80 | $value = ob_get_clean(); 81 | 82 | preg_match('/resource\(\d+\) of type \(([^)]+)\)/', $value, $matches); 83 | $this->assertSame('persistent stream', $matches[1]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/ReadPropertyHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Reflection\ReflectionValue; 18 | 19 | /** 20 | * Receiving hook for object field read operation 21 | */ 22 | class ReadPropertyHook extends AbstractPropertyHook 23 | { 24 | protected const HOOK_FIELD = 'read_property'; 25 | 26 | /** 27 | * Hook access type 28 | */ 29 | protected int $type; 30 | 31 | /** 32 | * Internal pointer of retval (for native callback only) 33 | */ 34 | private ?CData $rv; 35 | 36 | /** 37 | * typedef zval *(*zend_object_read_property_t)(zend_object *object, zend_string *member, int type, void **cache_slot, zval *rv); 38 | * 39 | * @inheritDoc 40 | */ 41 | public function handle(...$rawArguments): CData 42 | { 43 | [$this->object, $this->member, $this->type, $this->cacheSlot, $this->rv] = $rawArguments; 44 | 45 | $result = ($this->userHandler)($this); 46 | $refValue = new ReflectionValue($result); 47 | 48 | return $refValue->getRawValue(); 49 | } 50 | 51 | /** 52 | * Returns the access type 53 | */ 54 | public function getAccessType(): int 55 | { 56 | return $this->type; 57 | } 58 | 59 | /** 60 | * Proceeds with default handler 61 | */ 62 | public function proceed() 63 | { 64 | if (!$this->hasOriginalHandler()) { 65 | throw new \LogicException('Original handler is not available'); 66 | } 67 | 68 | // As we will play with EG(fake_scope), we won't be able to access private or protected members, need to unpack 69 | $originalHandler = $this->originalHandler; 70 | 71 | $object = $this->object; 72 | $member = $this->member; 73 | $type = $this->type; 74 | $cacheSlot = $this->cacheSlot; 75 | $rv = $this->rv; 76 | 77 | $previousScope = Core::$executor->setFakeScope($object->ce); 78 | $result = ($originalHandler)($object, $member, $type, $cacheSlot, $rv); 79 | Core::$executor->setFakeScope($previousScope); 80 | 81 | ReflectionValue::fromValueEntry($result)->getNativeValue($phpResult); 82 | 83 | return $phpResult; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Type/ReferenceEntry.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Reflection\ReflectionClass; 18 | use ZEngine\Reflection\ReflectionValue; 19 | 20 | /** 21 | * Class ReferenceEntry represents a reference instance in PHP 22 | * 23 | * struct _zend_reference { 24 | * zend_refcounted_h gc; 25 | * zval val; 26 | * zend_property_info_source_list sources; 27 | * }; 28 | */ 29 | class ReferenceEntry implements ReferenceCountedInterface 30 | { 31 | use ReferenceCountedTrait; 32 | 33 | private CData $pointer; 34 | 35 | public function __construct(&$reference) 36 | { 37 | // This code is used to extract a Zval for our $value argument and use its internal pointer 38 | $valueArgument = Core::$executor->getExecutionState()->getArgument(0); 39 | $pointer = $valueArgument->getRawReference(); 40 | $this->pointer = $pointer; 41 | } 42 | 43 | /** 44 | * Creates a resource entry from the zend_resource structure 45 | * 46 | * @param CData $pointer Pointer to the structure 47 | */ 48 | public static function fromCData(CData $pointer): ReferenceEntry 49 | { 50 | /** @var ReferenceEntry $referenceEntry */ 51 | $referenceEntry = (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); 52 | $referenceEntry->pointer = $pointer; 53 | 54 | return $referenceEntry; 55 | } 56 | 57 | /** 58 | * Returns the internal value, stored for this reference 59 | */ 60 | public function getValue(): ReflectionValue 61 | { 62 | return ReflectionValue::fromValueEntry($this->pointer->val); 63 | } 64 | 65 | /** 66 | * This method returns a dumpable representation of internal value to prevent segfault 67 | */ 68 | public function __debugInfo(): array 69 | { 70 | $info = [ 71 | 'refcount' => $this->getReferenceCount(), 72 | 'value' => $this->getValue() 73 | ]; 74 | 75 | return $info; 76 | } 77 | 78 | /** 79 | * This method should return an instance of zend_refcounted_h 80 | */ 81 | protected function getGC(): CData 82 | { 83 | return $this->pointer->gc; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/HasPropertyHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Hook\AbstractHook; 18 | use ZEngine\Reflection\ReflectionValue; 19 | 20 | /** 21 | * Receiving hook for object field check operation 22 | */ 23 | class HasPropertyHook extends AbstractPropertyHook 24 | { 25 | protected const HOOK_FIELD = 'has_property'; 26 | 27 | /** 28 | * Check type: 29 | * - 0 (has) whether property exists and is not NULL 30 | * - 1 (set) whether property exists and is true 31 | * - 2 (exists) whether property exists 32 | */ 33 | protected int $type; 34 | 35 | /** 36 | * typedef int (*zend_object_has_property_t)(zend_object *object, zend_string *member, int has_set_exists, void **cache_slot); 37 | * 38 | * @inheritDoc 39 | */ 40 | public function handle(...$rawArguments): int 41 | { 42 | [$this->object, $this->member, $this->type, $this->cacheSlot] = $rawArguments; 43 | 44 | $result = ($this->userHandler)($this); 45 | 46 | return $result; 47 | } 48 | 49 | /** 50 | * Returns the check type: 51 | * - 0 (has) whether property exists and is not NULL 52 | * - 1 (set) whether property exists and is true 53 | * - 2 (exists) whether property exists 54 | */ 55 | public function getType(): int 56 | { 57 | return $this->type; 58 | } 59 | 60 | /** 61 | * Proceeds with default handler 62 | */ 63 | public function proceed(): int 64 | { 65 | if (!$this->hasOriginalHandler()) { 66 | throw new \LogicException('Original handler is not available'); 67 | } 68 | 69 | // As we will play with EG(fake_scope), we won't be able to access private or protected members, need to unpack 70 | $originalHandler = $this->originalHandler; 71 | 72 | $object = $this->object; 73 | $member = $this->member; 74 | $type = $this->type; 75 | $cacheSlot = $this->cacheSlot; 76 | 77 | $previousScope = Core::$executor->setFakeScope($object->ce); 78 | $result = ($originalHandler)($object, $member, $type, $cacheSlot); 79 | Core::$executor->setFakeScope($previousScope); 80 | 81 | return $result; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/GetPropertiesForHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Hook\AbstractHook; 18 | use ZEngine\Reflection\ReflectionValue; 19 | use ZEngine\Type\ObjectEntry; 20 | 21 | /** 22 | * Receiving hook for casting to array, debugging, etc 23 | */ 24 | class GetPropertiesForHook extends AbstractHook 25 | { 26 | 27 | protected const HOOK_FIELD = 'get_properties_for'; 28 | 29 | /** 30 | * Object instance 31 | */ 32 | protected CData $object; 33 | 34 | /** 35 | * Calling reason 36 | * 37 | * @see zend_prop_purpose enumeration 38 | */ 39 | protected int $purpose; 40 | 41 | /** 42 | * zend_array *(*zend_object_get_properties_for_t)(zend_object *object, zend_prop_purpose purpose); 43 | * 44 | * @inheritDoc 45 | */ 46 | public function handle(...$rawArguments) 47 | { 48 | [$this->object, $this->purpose] = $rawArguments; 49 | 50 | $result = ($this->userHandler)($this); 51 | $refValue = new ReflectionValue($result); 52 | 53 | return $refValue->getRawArray(); 54 | } 55 | 56 | /** 57 | * Returns an object instance 58 | */ 59 | public function getObject(): object 60 | { 61 | $objectInstance = ObjectEntry::fromCData($this->object)->getNativeValue(); 62 | 63 | return $objectInstance; 64 | } 65 | 66 | /** 67 | * Returns the purpose 68 | */ 69 | public function getPurpose(): int 70 | { 71 | return $this->purpose; 72 | } 73 | 74 | /** 75 | * Proceeds with default handler 76 | */ 77 | public function proceed() 78 | { 79 | if (!$this->hasOriginalHandler()) { 80 | throw new \LogicException('Original handler is not available'); 81 | } 82 | 83 | // As we will play with EG(fake_scope), we won't be able to access private or protected members, need to unpack 84 | $originalHandler = $this->originalHandler; 85 | 86 | $object = $this->object; 87 | $purpose = $this->purpose; 88 | 89 | $previousScope = Core::$executor->setFakeScope($object->ce); 90 | $result = ($originalHandler)($object, $purpose); 91 | Core::$executor->setFakeScope($previousScope); 92 | 93 | return $result; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/AbstractSyntaxTree/ValueNode.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\AbstractSyntaxTree; 14 | 15 | use ZEngine\Core; 16 | use ZEngine\Reflection\ReflectionValue; 17 | 18 | /** 19 | * ValueNode stores a zval 20 | * 21 | * // Lineno is stored in val.u2.lineno 22 | * typedef struct _zend_ast_zval { 23 | * zend_ast_kind kind; 24 | * zend_ast_attr attr; 25 | * zval val; 26 | * } zend_ast_zval; 27 | * 28 | * @see zend_ast.h:zend_ast_zval 29 | */ 30 | class ValueNode extends Node 31 | { 32 | /** 33 | * Creates an AST node from value 34 | * 35 | * @param mixed $value Any valid value 36 | * @param int $attributes Additional attributes 37 | */ 38 | public function __construct($value, int $attributes = 0) 39 | { 40 | // This code is used to extract a Zval for our $value argument and use its internal pointer 41 | $valueArgument = Core::$executor->getExecutionState()->getArgument(0); 42 | $rawValue = $valueArgument->getRawValue(); 43 | 44 | $node = Core::call('zend_ast_create_zval_ex', $rawValue, $attributes); 45 | $node = Core::cast('zend_ast_zval *', $node); 46 | 47 | $this->node = $node; 48 | } 49 | 50 | /** 51 | * Returns current node value 52 | */ 53 | public function getValue(): ReflectionValue 54 | { 55 | return ReflectionValue::fromValueEntry($this->node->val); 56 | } 57 | 58 | /** 59 | * For ValueNode line is stored in the val.u2.lineno 60 | * 61 | * @inheritDoc 62 | */ 63 | public function getLine(): int 64 | { 65 | return $this->getValue()->getExtraValue(); 66 | } 67 | 68 | /** 69 | * @inheritDoc 70 | * Value node doesn't have children nodes 71 | */ 72 | public function getChildrenCount(): int 73 | { 74 | return 0; 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function dumpThis(int $indent = 0): string 81 | { 82 | $line = parent::dumpThis($indent); 83 | 84 | $line .= ' '; 85 | $this->getValue()->getNativeValue($value); 86 | if (is_scalar($value)) { 87 | $line .= gettype($value) . '(' . var_export($value, true) . ')'; 88 | } else { 89 | // shouldn't happen 90 | $line .= gettype($value) . "\n"; 91 | } 92 | 93 | return $line; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/WritePropertyHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Reflection\ReflectionValue; 18 | 19 | /** 20 | * Receiving hook for object field write operation 21 | */ 22 | class WritePropertyHook extends AbstractPropertyHook 23 | { 24 | protected const HOOK_FIELD = 'write_property'; 25 | 26 | /** 27 | * Value to write 28 | */ 29 | protected CData $value; 30 | 31 | /** 32 | * typedef zval *(*zend_object_write_property_t)(zend_object *object, zend_string *member, zval *value, void **cache_slot); 33 | * 34 | * @inheritDoc 35 | */ 36 | public function handle(...$rawArguments): CData 37 | { 38 | [$this->object, $this->member, $this->value, $this->cacheSlot] = $rawArguments; 39 | 40 | $result = ($this->userHandler)($this); 41 | ReflectionValue::fromValueEntry($this->value)->setNativeValue($result); 42 | 43 | return $this->proceed(); 44 | } 45 | 46 | /** 47 | * Returns value to write 48 | */ 49 | public function getValue() 50 | { 51 | ReflectionValue::fromValueEntry($this->value)->getNativeValue($value); 52 | 53 | return $value; 54 | } 55 | 56 | /** 57 | * Returns value to write 58 | * 59 | * @param mixed $newValue Value to set 60 | */ 61 | public function setValue($newValue) 62 | { 63 | ReflectionValue::fromValueEntry($this->value)->setNativeValue($newValue); 64 | } 65 | 66 | /** 67 | * Proceeds with default handler 68 | */ 69 | protected function proceed() 70 | { 71 | if (!$this->hasOriginalHandler()) { 72 | throw new \LogicException('Original handler is not available'); 73 | } 74 | 75 | // As we will play with EG(fake_scope), we won't be able to access private or protected members, need to unpack 76 | $originalHandler = $this->originalHandler; 77 | 78 | $object = $this->object; 79 | $member = $this->member; 80 | $value = $this->value; 81 | $cacheSlot = $this->cacheSlot; 82 | 83 | $previousScope = Core::$executor->setFakeScope($object->ce); 84 | $result = ($originalHandler)($object, $member, $value, $cacheSlot); 85 | Core::$executor->setFakeScope($previousScope); 86 | 87 | return $result; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionFunction.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use FFI\CData; 16 | use ReflectionFunction as NativeReflectionFunction; 17 | use ZEngine\Core; 18 | use ZEngine\Type\StringEntry; 19 | 20 | class ReflectionFunction extends NativeReflectionFunction 21 | { 22 | use FunctionLikeTrait; 23 | 24 | public function __construct(string $functionName) 25 | { 26 | parent::__construct($functionName); 27 | 28 | $normalizedName = strtolower($functionName); 29 | $functionEntryValue = Core::$executor->functionTable->find($normalizedName); 30 | if ($functionEntryValue === null) { 31 | throw new \ReflectionException("Function {$functionName} should be in the engine."); 32 | } 33 | $this->pointer = $functionEntryValue->getRawFunction(); 34 | } 35 | 36 | /** 37 | * Creates a reflection from the zend_function structure 38 | * 39 | * @param CData $functionEntry Pointer to the structure 40 | * 41 | * @return ReflectionFunction 42 | */ 43 | public static function fromCData(CData $functionEntry): ReflectionFunction 44 | { 45 | /** @var ReflectionFunction $reflectionFunction */ 46 | $reflectionFunction = (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); 47 | if ($functionEntry->type === Core::ZEND_INTERNAL_FUNCTION) { 48 | $functionNamePtr = $functionEntry->function_name; 49 | } else { 50 | $functionNamePtr = $functionEntry->common->function_name; 51 | } 52 | if ($functionNamePtr !== null) { 53 | $functionName = StringEntry::fromCData($functionNamePtr); 54 | call_user_func( 55 | [$reflectionFunction, 'parent::__construct'], 56 | $functionName->getStringValue() 57 | ); 58 | } 59 | $reflectionFunction->pointer = $functionEntry; 60 | 61 | return $reflectionFunction; 62 | } 63 | 64 | /** 65 | * Returns a user-friendly representation of internal structure to prevent segfault 66 | */ 67 | public function __debugInfo(): array 68 | { 69 | return [ 70 | 'name' => $this->getName(), 71 | ]; 72 | } 73 | 74 | /** 75 | * Returns the hash key for function or method 76 | */ 77 | protected function getHash(): string 78 | { 79 | return $this->name; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/CastObjectHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Hook\AbstractHook; 18 | use ZEngine\Reflection\ReflectionValue; 19 | use ZEngine\Type\ObjectEntry; 20 | 21 | /** 22 | * Receiving hook for casting object to another type 23 | */ 24 | class CastObjectHook extends AbstractHook 25 | { 26 | protected const HOOK_FIELD = 'cast_object'; 27 | 28 | /** 29 | * Object instance to perform casting 30 | */ 31 | protected CData $object; 32 | 33 | /** 34 | * Holds a return value 35 | */ 36 | protected CData $returnValue; 37 | 38 | /** 39 | * Cast type 40 | */ 41 | protected int $type; 42 | 43 | /** 44 | * typedef int (*zend_object_cast_t)(zend_object *readobj, zval *retval, int type); 45 | * 46 | * @inheritDoc 47 | */ 48 | public function handle(...$rawArguments): int 49 | { 50 | [$this->object, $this->returnValue, $this->type] = $rawArguments; 51 | 52 | $result = ($this->userHandler)($this); 53 | ReflectionValue::fromValueEntry($this->returnValue)->setNativeValue($result); 54 | 55 | return Core::SUCCESS; 56 | } 57 | 58 | /** 59 | * Returns the cast type 60 | * 61 | * @see ReflectionValue class constants, like ReflectionValue::IS_DOUBLE 62 | */ 63 | public function getCastType(): int 64 | { 65 | return $this->type; 66 | } 67 | 68 | /** 69 | * Returns an object instance 70 | */ 71 | public function getObject(): object 72 | { 73 | $objectInstance = ObjectEntry::fromCData($this->object)->getNativeValue(); 74 | 75 | return $objectInstance; 76 | } 77 | 78 | /** 79 | * Returns result of casting (eg from call to proceed) 80 | */ 81 | public function getResult() 82 | { 83 | ReflectionValue::fromValueEntry($this->returnValue)->getNativeValue($result); 84 | 85 | return $result; 86 | } 87 | 88 | /** 89 | * Proceeds with object casting 90 | */ 91 | public function proceed() 92 | { 93 | if (!$this->hasOriginalHandler()) { 94 | throw new \LogicException('Original handler is not available'); 95 | } 96 | $result = ($this->originalHandler)($this->object, $this->returnValue, $this->type); 97 | 98 | return $result; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/System/ObjectStoreTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | */ 10 | declare(strict_types=1); 11 | 12 | namespace ZEngine\System; 13 | 14 | use LogicException; 15 | use PHPUnit\Framework\TestCase; 16 | use ZEngine\Core; 17 | use ZEngine\Type\ObjectEntry; 18 | 19 | class ObjectStoreTest extends TestCase 20 | { 21 | private ObjectStore $objectStore; 22 | 23 | protected function setUp(): void 24 | { 25 | $this->objectStore = Core::$executor->objectStore; 26 | } 27 | 28 | public function testOffsetUnsetThrowsAnException(): void 29 | { 30 | $this->expectException(LogicException::class); 31 | $id = spl_object_id($this); 32 | unset($this->objectStore[$id]); 33 | } 34 | 35 | public function testOffsetSetThrowsAnException(): void 36 | { 37 | $this->expectException(LogicException::class); 38 | $id = spl_object_id($this); 39 | $this->objectStore[$id] = new ObjectEntry($this); 40 | } 41 | 42 | public function testOffsetGetReturnsObjects(): void 43 | { 44 | $id = spl_object_id($this); 45 | $objectEntry = $this->objectStore->offsetGet($id); 46 | $this->assertInstanceOf(ObjectEntry::class, $objectEntry); 47 | $this->assertSame($this, $objectEntry->getNativeValue()); 48 | 49 | // Now let's create new object and check that it's still accessible 50 | $newObject = new \stdClass(); 51 | $id = spl_object_id($newObject); 52 | $objectEntry = $this->objectStore->offsetGet($id); 53 | $this->assertSame($newObject, $objectEntry->getNativeValue()); 54 | } 55 | 56 | public function testOffsetExists(): void 57 | { 58 | $id = spl_object_id($this); 59 | $this->assertTrue($this->objectStore->offsetExists($id)); 60 | $this->assertTrue(isset($this->objectStore[$id])); 61 | } 62 | 63 | public function testCount(): void 64 | { 65 | $currentCount = count($this->objectStore); 66 | $this->assertGreaterThan(0, $currentCount); 67 | // We cannot predict the size of objectStore, because it can reuse previously deleted slots 68 | } 69 | 70 | public function testNextHandle(): void 71 | { 72 | // We can predict what will be the next handle of object 73 | $expectedHandle = $this->objectStore->nextHandle(); 74 | $object = new \stdClass(); 75 | $objectHandle = spl_object_id($object); 76 | $nextHandle = $this->objectStore->nextHandle(); 77 | 78 | $this->assertSame($expectedHandle, $objectHandle); 79 | $this->assertNotSame($expectedHandle, $nextHandle); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Hook/AbstractHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Hook; 14 | 15 | use Closure; 16 | use FFI; 17 | use FFI\CData; 18 | 19 | /** 20 | * AbstractHook provides reusable template for installing a hook in the PHP engine 21 | */ 22 | abstract class AbstractHook implements HookInterface 23 | { 24 | /** 25 | * This field should be updated in children class and accessed through LSB 26 | */ 27 | protected const HOOK_FIELD = 'unknown'; 28 | 29 | /** 30 | * Custom user handler 31 | */ 32 | protected Closure $userHandler; 33 | 34 | /** 35 | * Holds an original handler (if present) 36 | */ 37 | protected ?CData $originalHandler; 38 | 39 | /** 40 | * Contains a top-level structure that contains a field with hook 41 | * 42 | * @var CData|FFI Either raw C structure or global FFI object itself 43 | */ 44 | private $rawStructure; 45 | 46 | public function __construct(Closure $userHandler, $rawStructure) 47 | { 48 | assert($rawStructure instanceof FFI || $rawStructure instanceof CData, 'Invalid container'); 49 | $this->userHandler = $userHandler; 50 | $this->rawStructure = $rawStructure; 51 | $this->originalHandler = $rawStructure->{static::HOOK_FIELD}; 52 | } 53 | 54 | /** 55 | * Performs installation of current hook 56 | * 57 | * WARNING! 58 | * Please note, that this functionality is not supported on all libffi platforms, is not efficient and leaks 59 | * resources by the end of request. 60 | * 61 | * @link https://www.php.net/manual/en/ffi.examples-callback.php 62 | */ 63 | final public function install(): void 64 | { 65 | $this->rawStructure->{static::HOOK_FIELD} = Closure::fromCallable([$this, 'handle']); 66 | } 67 | 68 | /** 69 | * Checks if an original handler is present to call it later with proceed 70 | */ 71 | final public function hasOriginalHandler(): bool 72 | { 73 | return $this->originalHandler !== null; 74 | } 75 | 76 | /** 77 | * Automatic hook restore, this destructor ensures that there won't be any dead C pointers to PHP structures 78 | */ 79 | final public function __destruct() 80 | { 81 | $this->rawStructure->{static::HOOK_FIELD} = $this->originalHandler; 82 | } 83 | 84 | /** 85 | * Internal CData fields could result in segfaults, so let's hide everything 86 | */ 87 | final public function __debugInfo(): array 88 | { 89 | return [ 90 | 'userHandler' => $this->userHandler 91 | ]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ClassExtension/Hook/DoOperationHook.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\ClassExtension\Hook; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Hook\AbstractHook; 18 | use ZEngine\Reflection\ReflectionValue; 19 | 20 | /** 21 | * Receiving hook for performing operation on object 22 | */ 23 | class DoOperationHook extends AbstractHook 24 | { 25 | protected const HOOK_FIELD = 'do_operation'; 26 | 27 | /** 28 | * Operation opcode 29 | */ 30 | protected int $opCode; 31 | 32 | /** 33 | * Holds a return value 34 | */ 35 | protected CData $returnValue; 36 | 37 | /** 38 | * First operand 39 | */ 40 | protected CData $op1; 41 | 42 | /** 43 | * Second operand 44 | */ 45 | protected CData $op2; 46 | 47 | /** 48 | * typedef int (*zend_object_do_operation_t)(zend_uchar opcode, zval *result, zval *op1, zval *op2); 49 | * 50 | * @inheritDoc 51 | */ 52 | public function handle(...$rawArguments): int 53 | { 54 | [$this->opCode, $this->returnValue, $this->op1, $this->op2] = $rawArguments; 55 | 56 | $result = ($this->userHandler)($this); 57 | ReflectionValue::fromValueEntry($this->returnValue)->setNativeValue($result); 58 | 59 | return Core::SUCCESS; 60 | } 61 | 62 | /** 63 | * Returns an opcode 64 | */ 65 | public function getOpcode(): int 66 | { 67 | return $this->opCode; 68 | } 69 | 70 | /** 71 | * Returns first operand 72 | */ 73 | public function getFirst() 74 | { 75 | ReflectionValue::fromValueEntry($this->op1)->getNativeValue($value); 76 | 77 | return $value; 78 | } 79 | 80 | /** 81 | * Returns second operand 82 | */ 83 | public function getSecond() 84 | { 85 | ReflectionValue::fromValueEntry($this->op2)->getNativeValue($value); 86 | 87 | return $value; 88 | } 89 | 90 | /** 91 | * Returns result of casting (eg from call to proceed) 92 | */ 93 | public function getResult() 94 | { 95 | ReflectionValue::fromValueEntry($this->returnValue)->getNativeValue($result); 96 | 97 | return $result; 98 | } 99 | 100 | /** 101 | * Proceeds with object custom operation 102 | */ 103 | public function proceed() 104 | { 105 | if (!$this->hasOriginalHandler()) { 106 | throw new \LogicException('Original handler is not available'); 107 | } 108 | $result = ($this->originalHandler)($this->opCode, $this->returnValue, $this->op1, $this->op2); 109 | 110 | return $result; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /tests/Reflection/ReflectionFunctionTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use PHPUnit\Framework\Error\Deprecated; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | /** 19 | * Test function to reflect 20 | */ 21 | function testFunction(): ?string 22 | { 23 | return 'Test'; 24 | } 25 | 26 | class ReflectionFunctionTest extends TestCase 27 | { 28 | private ReflectionFunction $refFunction; 29 | 30 | protected function setUp(): void 31 | { 32 | $this->refFunction = new ReflectionFunction(__NAMESPACE__ . '\\' . 'testFunction'); 33 | } 34 | 35 | public function testSetDeprecated(): void 36 | { 37 | $this->markTestSkipped('User function does not trigger deprecation error'); 38 | } 39 | 40 | public function testSetInternalFunctionDeprecated(): void 41 | { 42 | try { 43 | $currentReporting = error_reporting(); 44 | error_reporting(E_ALL); 45 | $refFunction = new ReflectionFunction('var_dump'); 46 | $refFunction->setDeprecated(); 47 | $this->assertTrue($refFunction->isDeprecated()); 48 | 49 | $this->expectException(Deprecated::class); 50 | $this->expectExceptionMessageMatches('/Function var_dump\(\) is deprecated/'); 51 | var_dump($currentReporting); 52 | } finally { 53 | error_reporting($currentReporting); 54 | $refFunction->setDeprecated(false); 55 | } 56 | } 57 | 58 | /** 59 | * @group internal 60 | */ 61 | public function testRedefineThrowsAnExceptionForIncompatibleCallback(): void 62 | { 63 | $this->expectException(\ReflectionException::class); 64 | $expectedRegexp = '/"function \(\)" should be compatible with original "function \(\)\: \?string"/'; 65 | $this->expectExceptionMessageMatches($expectedRegexp); 66 | 67 | $this->refFunction->redefine(function () { 68 | echo 'Nope'; 69 | }); 70 | } 71 | 72 | /** 73 | * @group internal 74 | */ 75 | public function testRedefine(): void 76 | { 77 | $this->refFunction->redefine(function (): ?string { 78 | return 'Yes'; 79 | }); 80 | // Check that all main info were preserved 81 | $this->assertFalse($this->refFunction->isClosure()); 82 | $this->assertSame('testFunction', $this->refFunction->getShortName()); 83 | 84 | $result = testFunction(); 85 | 86 | // Our function now returns Yes instead of Test 87 | $this->assertSame('Yes', $result); 88 | } 89 | 90 | /** 91 | * @group internal 92 | */ 93 | public function testRedefineInternalFunc(): void 94 | { 95 | $originalValue = zend_version(); 96 | $refFunction = new ReflectionFunction('zend_version'); 97 | 98 | $refFunction->redefine(function (): string { 99 | return 'Z-Engine'; 100 | }); 101 | 102 | $modifiedValue = zend_version(); 103 | $this->assertNotSame($originalValue, $modifiedValue); 104 | $this->assertSame('Z-Engine', $modifiedValue); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Reflection/ReflectionClassConstantTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use Error; 16 | use PHPUnit\Framework\TestCase; 17 | use ZEngine\Stub\TestClass; 18 | 19 | class ReflectionClassConstantTest extends TestCase 20 | { 21 | private ReflectionClassConstant $refConstant; 22 | 23 | protected function setUp(): void 24 | { 25 | $this->refConstant = new ReflectionClassConstant(TestClass::class, 'SOME_CONST'); 26 | } 27 | 28 | public function testSetPrivate(): void 29 | { 30 | $this->refConstant->setPrivate(); 31 | $this->assertTrue($this->refConstant->isPrivate()); 32 | $this->assertFalse($this->refConstant->isPublic()); 33 | $this->assertFalse($this->refConstant->isProtected()); 34 | 35 | $this->expectException(Error::class); 36 | $this->expectExceptionMessageMatches('/Cannot access private const\S* .*?SOME_CONST/'); 37 | $this->assertSame(123, TestClass::SOME_CONST); 38 | } 39 | 40 | /** 41 | * @depends testSetPrivate 42 | */ 43 | public function testSetProtected(): void 44 | { 45 | $this->refConstant->setProtected(); 46 | $this->assertTrue($this->refConstant->isProtected()); 47 | $this->assertFalse($this->refConstant->isPrivate()); 48 | $this->assertFalse($this->refConstant->isPublic()); 49 | 50 | // We can override+call protected method from child by making it public 51 | $child = new class extends TestClass { 52 | public function getConstant() 53 | { 54 | // return parent const which is protected now 55 | return parent::SOME_CONST; 56 | } 57 | }; 58 | $value = $child->getConstant(); 59 | $this->assertSame(123, $value); 60 | 61 | // If we try to access our protected constant, we should have an error here 62 | $this->expectException(Error::class); 63 | $this->expectExceptionMessageMatches('/Cannot access protected const\S* .*?SOME_CONST/'); 64 | $this->assertSame(123, TestClass::SOME_CONST); 65 | } 66 | 67 | /** 68 | * @depends testSetProtected 69 | */ 70 | public function testSetPublic(): void 71 | { 72 | $this->refConstant->setPublic(); 73 | $this->assertTrue($this->refConstant->isPublic()); 74 | $this->assertFalse($this->refConstant->isPrivate()); 75 | $this->assertFalse($this->refConstant->isProtected()); 76 | 77 | $this->assertSame(123, TestClass::SOME_CONST); 78 | } 79 | 80 | public function testGetDeclaringClassReturnsCorrectInstance(): void 81 | { 82 | $class = $this->refConstant->getDeclaringClass(); 83 | $this->assertInstanceOf(ReflectionClass::class, $class); 84 | $this->assertSame(TestClass::class, $class->getName()); 85 | } 86 | 87 | /** 88 | * @group internal 89 | */ 90 | public function testSetDeclaringClass(): void 91 | { 92 | try { 93 | $this->refConstant->setDeclaringClass(self::class); 94 | $this->assertSame(self::class, $this->refConstant->getDeclaringClass()->getName()); 95 | } finally { 96 | $this->refConstant->setDeclaringClass(TestClass::class); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Type/ResourceEntry.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Reflection\ReflectionClass; 18 | use ZEngine\Reflection\ReflectionValue; 19 | 20 | /** 21 | * Class ResourceEntry represents a resource instance in PHP 22 | * 23 | * struct _zend_resource { 24 | * zend_refcounted_h gc; 25 | * int handle; // TODO: may be removed ??? 26 | * int type; 27 | * void *ptr; 28 | * }; 29 | * 30 | * @link https://github.com/php/php-src/blob/master/Zend/zend_types.h 31 | */ 32 | class ResourceEntry implements ReferenceCountedInterface 33 | { 34 | use ReferenceCountedTrait; 35 | 36 | private CData $pointer; 37 | 38 | public function __construct($resource) 39 | { 40 | if (!is_resource($resource)) { 41 | throw new \InvalidArgumentException('Only resource type is accepted'); 42 | } 43 | $reflectionValue = new ReflectionValue($resource); 44 | $this->pointer = $reflectionValue->getRawResource(); 45 | } 46 | 47 | /** 48 | * Creates a resource entry from the zend_resource structure 49 | */ 50 | public static function fromCData(CData $pointer): ResourceEntry 51 | { 52 | /** @var ResourceEntry $resourceEntry */ 53 | $resourceEntry = (new ReflectionClass(self::class))->newInstanceWithoutConstructor(); 54 | $resourceEntry->pointer = $pointer; 55 | 56 | return $resourceEntry; 57 | } 58 | 59 | /** 60 | * Returns the internal type identifier for this resource 61 | */ 62 | public function getType(): int 63 | { 64 | return $this->pointer->type; 65 | } 66 | 67 | /** 68 | * Returns a resource handle 69 | */ 70 | public function getHandle(): int 71 | { 72 | return $this->pointer->handle; 73 | } 74 | 75 | /** 76 | * Returns the low-level raw data, associated with this resource 77 | */ 78 | public function getRawData(): CData 79 | { 80 | return $this->pointer->ptr; 81 | } 82 | 83 | /** 84 | * Changes the internal type identifier for this resource 85 | * 86 | * Danger! Low-level API, can bring a segmentation fault 87 | * @internal 88 | */ 89 | public function setType(int $newType): void 90 | { 91 | $this->pointer->type = $newType; 92 | } 93 | 94 | /** 95 | * Changes object internal handle to another one 96 | * @internal 97 | */ 98 | public function setHandle(int $newHandle): void 99 | { 100 | $this->pointer->handle = $newHandle; 101 | } 102 | 103 | /** 104 | * This method returns a dumpable representation of internal value to prevent segfault 105 | */ 106 | public function __debugInfo(): array 107 | { 108 | $info = [ 109 | 'type' => $this->getType(), 110 | 'handle' => $this->getHandle(), 111 | 'refcount' => $this->getReferenceCount(), 112 | 'data' => $this->getRawData() 113 | ]; 114 | 115 | return $info; 116 | } 117 | 118 | /** 119 | * This method should return an instance of zend_refcounted_h 120 | */ 121 | protected function getGC(): CData 122 | { 123 | return $this->pointer->gc; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Type/ClosureEntry.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use ReflectionClass as NativeReflectionClass; 17 | use ZEngine\Core; 18 | 19 | /** 20 | * Class ClosureEntry 21 | * 22 | * typedef struct _zend_closure { 23 | * zend_object std; 24 | * zend_function func; 25 | * zval this_ptr; 26 | * zend_class_entry *called_scope; 27 | * zif_handler orig_internal_handler; 28 | * } zend_closure; 29 | */ 30 | class ClosureEntry 31 | { 32 | private CData $pointer; 33 | 34 | public function __construct(\Closure $closure) 35 | { 36 | $selfExecutionState = Core::$executor->getExecutionState(); 37 | $closureEntry = $selfExecutionState->getArgument(0)->getRawObject(); 38 | $this->pointer = Core::cast('zend_closure *', $closureEntry); 39 | } 40 | 41 | /** 42 | * Creates a closure entry from the zend_closure structure 43 | * 44 | * @param CData $pointer Pointer to the structure 45 | */ 46 | public static function fromCData(CData $pointer): ClosureEntry 47 | { 48 | /** @var ClosureEntry $closureEntry */ 49 | $closureEntry = (new NativeReflectionClass(static::class))->newInstanceWithoutConstructor(); 50 | $closureEntry->pointer = $pointer; 51 | 52 | return $closureEntry; 53 | } 54 | 55 | /** 56 | * Returns a raw object that represents this closure 57 | */ 58 | public function getClosureObjectEntry(): ObjectEntry 59 | { 60 | return ObjectEntry::fromCData($this->pointer->std); 61 | } 62 | 63 | /** 64 | * Returns the called scope (if present), otherwise null for unbound closures 65 | */ 66 | public function getCalledScope(): ?string 67 | { 68 | if ($this->pointer->called_scope === null) { 69 | return null; 70 | } 71 | 72 | $calledScopeName = StringEntry::fromCData($this->pointer->called_scope->name); 73 | 74 | return $calledScopeName->getStringValue(); 75 | } 76 | 77 | /** 78 | * Changes the scope of closure to another one 79 | * @internal 80 | */ 81 | public function setCalledScope(?string $newScope): void 82 | { 83 | // If we have a null value, then just clean this scope internally 84 | if ($newScope === null) { 85 | $this->pointer->called_scope = null; 86 | return; 87 | } 88 | 89 | $name = strtolower($newScope); 90 | 91 | $classEntryValue = Core::$executor->classTable->find($name); 92 | if ($classEntryValue === null) { 93 | throw new \ReflectionException("Class {$newScope} was not found"); 94 | } 95 | $this->pointer->called_scope = $classEntryValue->getRawClass(); 96 | } 97 | 98 | /** 99 | * Changes the current $this, bound to the closure 100 | * 101 | * Warning! Given object should live more than closure itself! 102 | * @param object $object New object 103 | * 104 | * @internal 105 | */ 106 | public function setThis(object $object): void 107 | { 108 | $selfExecutionState = Core::$executor->getExecutionState(); 109 | $objectArgument = $selfExecutionState->getArgument(0); 110 | $objectZval = $objectArgument->getRawValue(); 111 | Core::memcpy($this->pointer->this_ptr, $objectZval[0], Core::sizeof(Core::type('zval'))); 112 | } 113 | 114 | /** 115 | * Returns raw zend_function data for this closure 116 | */ 117 | public function getRawFunction(): CData 118 | { 119 | return $this->pointer->func; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/Stub/NativeNumber.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Stub; 14 | 15 | use ZEngine\ClassExtension\Hook\CastObjectHook; 16 | use ZEngine\ClassExtension\Hook\CompareValuesHook; 17 | use ZEngine\ClassExtension\Hook\DoOperationHook; 18 | use ZEngine\ClassExtension\ObjectCastInterface; 19 | use ZEngine\ClassExtension\ObjectCompareValuesInterface; 20 | use ZEngine\ClassExtension\ObjectCreateInterface; 21 | use ZEngine\ClassExtension\ObjectCreateTrait; 22 | use ZEngine\ClassExtension\ObjectDoOperationInterface; 23 | use ZEngine\Reflection\ReflectionValue; 24 | use ZEngine\System\OpCode; 25 | 26 | class NativeNumber implements 27 | ObjectCreateInterface, 28 | ObjectCompareValuesInterface, 29 | ObjectDoOperationInterface, 30 | ObjectCastInterface 31 | { 32 | use ObjectCreateTrait; 33 | 34 | private $value; 35 | 36 | public function __construct($value) 37 | { 38 | if (!is_numeric($value)) { 39 | throw new \InvalidArgumentException('Only numeric values are allowed'); 40 | } 41 | $this->value = $value; 42 | } 43 | 44 | /** 45 | * @param NativeNumber $instance 46 | * @inheritDoc 47 | */ 48 | public static function __cast(CastObjectHook $hook) 49 | { 50 | $typeTo = $hook->getCastType(); 51 | switch ($typeTo) { 52 | case ReflectionValue::_IS_NUMBER: 53 | case ReflectionValue::IS_LONG: 54 | return (int) $hook->getObject()->value; 55 | case ReflectionValue::IS_DOUBLE: 56 | return (float) $hook->getObject()->value; 57 | } 58 | 59 | throw new \UnexpectedValueException('Can not cast number to the ' . ReflectionValue::name($typeTo)); 60 | } 61 | 62 | /** 63 | * Performs comparison of given object with another value 64 | * 65 | * @param CompareValuesHook $hook Instance of current hook 66 | * 67 | * @return int Result of comparison: 1 is greater, -1 is less, 0 is equal 68 | */ 69 | public static function __compare(CompareValuesHook $hook): int 70 | { 71 | $left = self::getNumericValue($hook->getFirst()); 72 | $right = self::getNumericValue($hook->getSecond()); 73 | 74 | return $left <=> $right; 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public static function __doOperation(DoOperationHook $hook) 81 | { 82 | $opCode = $hook->getOpcode(); 83 | $left = self::getNumericValue($hook->getFirst()); 84 | $right = self::getNumericValue($hook->getSecond()); 85 | switch ($opCode) { 86 | case OpCode::ADD: 87 | $result = $left + $right; 88 | break; 89 | case OpCode::SUB: 90 | $result = $left - $right; 91 | break; 92 | case OpCode::MUL: 93 | $result = $left * $right; 94 | break; 95 | case OpCode::DIV: 96 | $result = $left / $right; 97 | break; 98 | default: 99 | throw new \UnexpectedValueException("Opcode " . OpCode::name($opCode) . " wasn't held."); 100 | } 101 | 102 | return new static($result); 103 | } 104 | 105 | /** 106 | * @param $one 107 | * 108 | * @return int|string 109 | */ 110 | private static function getNumericValue($one) 111 | { 112 | if ($one instanceof NativeNumber) { 113 | $left = $one->value; 114 | } elseif (is_numeric($one)) { 115 | $left = $one; 116 | } else { 117 | throw new \UnexpectedValueException('NativeNumber can be compared only with numeric values and itself'); 118 | } 119 | 120 | return $left; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Macro/DefinitionLoader.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Macro; 14 | 15 | use php_user_filter as PhpStreamFilter; 16 | use RuntimeException; 17 | 18 | class DefinitionLoader extends PhpStreamFilter 19 | { 20 | /** 21 | * Default PHP filter name for registration 22 | */ 23 | private const FILTER_IDENTIFIER = 'z-engine.def.loader'; 24 | 25 | /** 26 | * String buffer 27 | */ 28 | private string $data = ''; 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function filter($in, $out, &$consumed, $closing) 34 | { 35 | /** Simple pattern to match if(n?)def..endif constructions */ 36 | static $pattern = '/^#(ifn?def) +(.*?)\n([\s\S]*?)(#endif)/m'; 37 | 38 | while ($bucket = stream_bucket_make_writeable($in)) { 39 | $this->data .= $bucket->data; 40 | } 41 | 42 | if ($closing || feof($this->stream)) { 43 | $consumed = strlen($this->data); 44 | 45 | $macros = $this->resolveSystemMacros(); 46 | // Now we emulate resolution of ifdef..endif constructions 47 | $transformedData = $this->data; 48 | $transformedData = preg_replace_callback($pattern, function (array $matches) use ($macros): string { 49 | [, $keyword, $macro, $body] = $matches; 50 | if ($keyword === 'ifdef' && !isset($macros[$macro])) { 51 | $body = ''; 52 | } elseif ($keyword === 'ifndef' && isset($macros[$macro])) { 53 | $body = ''; 54 | } 55 | 56 | return $body; 57 | }, $transformedData); 58 | 59 | // Simple macros resolving via strtr 60 | $transformedData = strtr($transformedData, $macros); 61 | 62 | $bucket = stream_bucket_new($this->stream, $transformedData); 63 | stream_bucket_append($out, $bucket); 64 | 65 | return PSFS_PASS_ON; 66 | } 67 | 68 | return PSFS_FEED_ME; 69 | } 70 | 71 | /** 72 | * Wraps given filename with stream resolver 73 | * 74 | * @param string $filename 75 | */ 76 | public static function wrap(string $filename): string 77 | { 78 | // Let's perform self-registration on first query 79 | if (!in_array(self::FILTER_IDENTIFIER, stream_get_filters(), true)) { 80 | self::register(); 81 | } 82 | 83 | return 'php://filter/read=' . self::FILTER_IDENTIFIER . '/resource=' . $filename; 84 | } 85 | 86 | /** 87 | * Register current loader as stream filter in PHP 88 | * 89 | * @throws RuntimeException If registration was failed 90 | */ 91 | private static function register(): void 92 | { 93 | $result = stream_filter_register(self::FILTER_IDENTIFIER, self::class); 94 | if ($result === false) { 95 | throw new RuntimeException('Stream filter was not registered'); 96 | } 97 | } 98 | 99 | private function resolveSystemMacros(): array 100 | { 101 | $isThreadSafe = ZEND_THREAD_SAFE; 102 | $isWindowsPlatform = stripos(PHP_OS, 'WIN') === 0; 103 | $is64BitPlatform = PHP_INT_SIZE === 8; 104 | 105 | // TODO: support ts/nts x86/x64 combination 106 | if ($isThreadSafe || !$is64BitPlatform) { 107 | throw new \RuntimeException('Only x64 non thread-safe versions of PHP are supported'); 108 | } 109 | 110 | $macros = [ 111 | 'ZEND_API' => '__declspec(dllimport)', 112 | 'ZEND_FASTCALL' => $isWindowsPlatform ? '__vectorcall' : '', 113 | 114 | 'ZEND_MAX_RESERVED_RESOURCES' => '6', 115 | 'INTERNAL_FUNCTION_PARAMETERS' => 'zend_execute_data *execute_data, zval *return_value', 116 | 'ZEND_LIBRARY_NAME' => $isWindowsPlatform ? 'php7.dll' : '', 117 | ]; 118 | 119 | if ($isWindowsPlatform) { 120 | $macros['ZEND_WIN32'] = '1'; 121 | } 122 | 123 | if ($isThreadSafe) { 124 | $macros['ZTS'] = '1'; 125 | } 126 | 127 | return $macros; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Type/StringEntry.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use ReflectionClass; 17 | use ZEngine\Core; 18 | use ZEngine\Reflection\ReflectionValue; 19 | 20 | /** 21 | * This class wraps PHP's zend_string structure and provide an API for working with it 22 | * 23 | * struct _zend_string { 24 | * zend_refcounted_h gc; 25 | * zend_ulong h; // hash value 26 | * size_t len; 27 | * char val[1]; 28 | * }; 29 | */ 30 | class StringEntry implements ReferenceCountedInterface 31 | { 32 | use ReferenceCountedTrait; 33 | 34 | private CData $pointer; 35 | 36 | /** 37 | * Creates a string entry from the PHP string 38 | */ 39 | public function __construct(string $value) 40 | { 41 | // This code is used to extract a Zval for our $value argument and use its internal pointer 42 | $valueArgument = Core::$executor->getExecutionState()->getArgument(0); 43 | $this->pointer = $valueArgument->getRawString()[0]; 44 | } 45 | 46 | /** 47 | * Creates a string entry from the zend_string structure 48 | * 49 | * @param CData $stringPointer Pointer to the structure 50 | */ 51 | public static function fromCData(CData $stringPointer): StringEntry 52 | { 53 | /** @var StringEntry $stringEntry */ 54 | $stringEntry = (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); 55 | $stringEntry->pointer = $stringPointer; 56 | 57 | return $stringEntry; 58 | } 59 | 60 | /** 61 | * Returns raw C value entry 62 | */ 63 | public function getRawValue(): ?CData 64 | { 65 | return $this->pointer; 66 | } 67 | 68 | /** 69 | * Returns a hash for given string 70 | */ 71 | public function getHash(): int 72 | { 73 | return $this->pointer->h; 74 | } 75 | 76 | /** 77 | * Returns a string length 78 | */ 79 | public function getLength(): int 80 | { 81 | return $this->pointer->len; 82 | } 83 | 84 | /** 85 | * Returns a PHP representation of engine string 86 | */ 87 | public function getStringValue(): string 88 | { 89 | $entry = ReflectionValue::newEntry(ReflectionValue::IS_STRING, $this->pointer[0]); 90 | $entry->getNativeValue($realString); 91 | 92 | // TODO: Incapsulate memory management into ReflectionValue->release() method 93 | Core::free($entry->getRawValue()); 94 | 95 | return $realString; 96 | } 97 | 98 | /** 99 | * This methods releases a string entry 100 | * 101 | * @see zend_string.h:zend_string_release function 102 | */ 103 | public function release(): void 104 | { 105 | if (!$this->isInterned()) { 106 | if ($this->decrementReferenceCount() === 0) { 107 | Core::free($this->pointer); 108 | } 109 | } 110 | } 111 | 112 | /** 113 | * Creates a copy of string value 114 | * 115 | * @see zend_string.h::zend_string_copy function 116 | * 117 | * @return self 118 | */ 119 | public function copy(): self 120 | { 121 | if (!$this->isInterned()) { 122 | $this->incrementReferenceCount(); 123 | } 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Alias to check if this string is interned (aka immutable) 130 | * 131 | * @return bool 132 | */ 133 | public function isInterned(): bool 134 | { 135 | return $this->isImmutable(); 136 | } 137 | 138 | /** 139 | * This method returns a dumpable representation of internal value to prevent segfault 140 | */ 141 | public function __debugInfo(): array 142 | { 143 | return [ 144 | 'value' => $this->getStringValue(), 145 | 'length' => $this->getLength(), 146 | 'refcount' => $this->getReferenceCount(), 147 | 'hash' => $this->getHash(), 148 | ]; 149 | } 150 | 151 | /** 152 | * This method should return an instance of zend_refcounted_h 153 | */ 154 | protected function getGC(): CData 155 | { 156 | return $this->pointer->gc; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/System/ObjectStore.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\System; 14 | 15 | use ArrayAccess; 16 | use Countable; 17 | use FFI\CData; 18 | use ZEngine\Core; 19 | use ZEngine\Type\ObjectEntry; 20 | 21 | final class ObjectStore implements Countable, ArrayAccess 22 | { 23 | /** 24 | * @see zend_objects_API.h:OBJ_BUCKET_INVALID macro 25 | */ 26 | private const OBJ_BUCKET_INVALID = 1<<0; 27 | 28 | /** 29 | * Holds an internal pointer to the EG(objects_store) 30 | */ 31 | private CData $pointer; 32 | 33 | public function __construct(CData $pointer) 34 | { 35 | $this->pointer = $pointer; 36 | } 37 | 38 | /** 39 | * @inheritDoc 40 | */ 41 | public function count(): int 42 | { 43 | return $this->pointer->top - 1; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | public function offsetExists($offset): bool 50 | { 51 | $isValidOffset = ($offset >= 0) && ($offset < $this->pointer->top); 52 | $isExists = $isValidOffset && $this->isObjectValid($this->pointer->object_buckets[$offset]); 53 | 54 | return $isExists; 55 | } 56 | 57 | /** 58 | * Returns an object from the storage by it's id or null if this object was released 59 | * 60 | * @param int $offset Identifier of object 61 | * 62 | * @see spl_object_id() 63 | */ 64 | public function offsetGet($offset): ?ObjectEntry 65 | { 66 | if (!\is_int($offset)) { 67 | throw new \InvalidArgumentException('Object identifier should be an integer'); 68 | } 69 | if ($offset < 0 || $offset > $this->pointer->top - 1) { 70 | // We use -2 because exception object also increments index by one 71 | throw new \OutOfBoundsException("Index {$offset} is out of bounds 0.." . ($this->pointer->top - 2)); 72 | } 73 | $object = $this->pointer->object_buckets[$offset]; 74 | 75 | // Object can be invalid, for that case we should return null 76 | if (!$this->isObjectValid($object)) { 77 | return null; 78 | } 79 | 80 | $objectEntry = ObjectEntry::fromCData($object); 81 | 82 | return $objectEntry; 83 | } 84 | 85 | /** 86 | * @inheritDoc 87 | */ 88 | public function offsetSet($offset, $value): void 89 | { 90 | throw new \LogicException('Object store is read-only structure'); 91 | } 92 | 93 | /** 94 | * @inheritDoc 95 | */ 96 | public function offsetUnset($offset): void 97 | { 98 | throw new \LogicException('Object store is read-only structure'); 99 | } 100 | 101 | /** 102 | * Returns the free head (aka next handle) 103 | */ 104 | public function nextHandle(): int 105 | { 106 | return $this->pointer->free_list_head; 107 | } 108 | 109 | /** 110 | * Detaches existing object from the object store 111 | * 112 | * Warning! This call doesn't invokes object destructors, 113 | * only detaches an object from the store. 114 | * 115 | * @see zend_objects_API.h:SET_OBJ_INVALID macro 116 | * @internal 117 | */ 118 | public function detach(int $offset): void 119 | { 120 | if ($offset < 0 || $offset > $this->pointer->top - 1) { 121 | // We use -2 because exception object also increments index by one 122 | throw new \OutOfBoundsException("Index {$offset} is out of bounds 0.." . ($this->pointer->top - 2)); 123 | } 124 | $rawPointer = Core::cast('zend_uintptr_t', $this->pointer->object_buckets[$offset]); 125 | $invalidPointer = $rawPointer->cdata | self::OBJ_BUCKET_INVALID; 126 | $rawPointer->cdata = $invalidPointer; 127 | 128 | $this->pointer->object_buckets[$offset] = Core::cast('zend_object *', $rawPointer); 129 | } 130 | 131 | /** 132 | * Checks if the given object pointer is valid or not 133 | * 134 | * @see zend_objects_API.h:IS_OBJ_VALID macro 135 | */ 136 | private function isObjectValid(?CData $objectPointer): bool 137 | { 138 | if ($objectPointer === null) { 139 | return false; 140 | } 141 | 142 | $rawPointer = Core::cast('zend_uintptr_t', $objectPointer); 143 | $isValid = ($rawPointer->cdata & self::OBJ_BUCKET_INVALID) === 0; 144 | 145 | return $isValid; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Type/ObjectEntry.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use ReflectionClass as NativeReflectionClass; 17 | use ZEngine\Core; 18 | use ZEngine\Reflection\ReflectionClass; 19 | use ZEngine\Reflection\ReflectionValue; 20 | 21 | /** 22 | * Class ObjectEntry represents an object instance in PHP 23 | * 24 | * struct _zend_object { 25 | * zend_refcounted_h gc; 26 | * uint32_t handle; 27 | * zend_class_entry *ce; 28 | * const zend_object_handlers *handlers; 29 | * HashTable *properties; 30 | * zval properties_table[1]; 31 | * }; 32 | */ 33 | class ObjectEntry implements ReferenceCountedInterface 34 | { 35 | use ReferenceCountedTrait; 36 | 37 | private HashTable $properties; 38 | 39 | private CData $pointer; 40 | 41 | public function __construct(object $instance) 42 | { 43 | $refValue = new ReflectionValue($instance); 44 | $pointer = $refValue->getRawObject(); 45 | $this->initLowLevelStructures($pointer); 46 | } 47 | 48 | /** 49 | * Creates an object entry from the zend_object structure 50 | * 51 | * @param CData $pointer Pointer to the structure 52 | */ 53 | public static function fromCData(CData $pointer): ObjectEntry 54 | { 55 | /** @var ObjectEntry $objectEntry */ 56 | $objectEntry = (new NativeReflectionClass(static::class))->newInstanceWithoutConstructor(); 57 | $objectEntry->initLowLevelStructures($pointer); 58 | 59 | return $objectEntry; 60 | } 61 | 62 | /** 63 | * Returns the class reflection for current object 64 | */ 65 | public function getClass(): ReflectionClass 66 | { 67 | return ReflectionClass::fromCData($this->pointer->ce); 68 | } 69 | 70 | /** 71 | * Changes the class of object to another one 72 | * 73 | * Danger! Low-level API, can bring a segmentation fault 74 | * @internal 75 | */ 76 | public function setClass(string $newClass): void 77 | { 78 | $classEntryValue = Core::$executor->classTable->find(strtolower($newClass)); 79 | if ($classEntryValue === null) { 80 | throw new \ReflectionException("Class {$newClass} was not found"); 81 | } 82 | $this->pointer->ce = $classEntryValue->getRawClass(); 83 | } 84 | 85 | /** 86 | * Returns an object handle, this should be equal to spl_object_id 87 | * 88 | * @see spl_object_id() 89 | */ 90 | public function getHandle(): int 91 | { 92 | return $this->pointer->handle; 93 | } 94 | 95 | /** 96 | * Changes object internal handle to another one 97 | * @internal 98 | */ 99 | public function setHandle(int $newHandle): void 100 | { 101 | $this->pointer->handle = $newHandle; 102 | } 103 | 104 | /** 105 | * Returns a PHP instance of object, associated with this entry 106 | */ 107 | public function getNativeValue(): object 108 | { 109 | $entry = ReflectionValue::newEntry(ReflectionValue::IS_OBJECT, $this->pointer[0]); 110 | $entry->getNativeValue($realObject); 111 | 112 | // TODO: Incapsulate memory management into ReflectionValue->release() method 113 | Core::free($entry->getRawValue()); 114 | 115 | return $realObject; 116 | } 117 | 118 | /** 119 | * This method returns a dumpable representation of internal value to prevent segfault 120 | */ 121 | public function __debugInfo(): array 122 | { 123 | $info = [ 124 | 'class' => $this->getClass()->getName(), 125 | 'handle' => $this->getHandle(), 126 | 'refcount' => $this->getReferenceCount() 127 | ]; 128 | if (isset($this->properties)) { 129 | $info['properties'] = $this->properties; 130 | } 131 | 132 | return $info; 133 | } 134 | 135 | /** 136 | * This method should return an instance of zend_refcounted_h 137 | */ 138 | protected function getGC(): CData 139 | { 140 | return $this->pointer->gc; 141 | } 142 | 143 | /** 144 | * Performs low-level initialization of object 145 | */ 146 | private function initLowLevelStructures(CData $pointer): void 147 | { 148 | $this->pointer = $pointer; 149 | if ($this->pointer->properties !== null) { 150 | $this->properties = new HashTable($this->pointer->properties); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Type/HashTable.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use IteratorAggregate; 17 | use Traversable; 18 | use ZEngine\Core; 19 | use ZEngine\Reflection\ReflectionValue; 20 | 21 | /** 22 | * Class HashTable provides general access to the internal array objects, aka hash-table 23 | * 24 | * struct _zend_array { 25 | * zend_refcounted_h gc; 26 | * union { 27 | * struct { 28 | * zend_uchar flags; 29 | * zend_uchar _unused; 30 | * zend_uchar nIteratorsCount; 31 | * zend_uchar _unused2; 32 | * } v; 33 | * uint32_t flags; 34 | * } u; 35 | * uint32_t nTableMask; 36 | * Bucket *arData; 37 | * uint32_t nNumUsed; 38 | * uint32_t nNumOfElements; 39 | * uint32_t nTableSize; 40 | * uint32_t nInternalPointer; 41 | * zend_long nNextFreeElement; 42 | * dtor_func_t pDestructor; 43 | * }; 44 | */ 45 | class HashTable implements IteratorAggregate, ReferenceCountedInterface 46 | { 47 | use ReferenceCountedTrait; 48 | 49 | private const HASH_UPDATE = (1 << 0); 50 | private const HASH_ADD = (1 << 1); 51 | private const HASH_UPDATE_INDIRECT = (1 << 2); 52 | private const HASH_ADD_NEW = (1 << 3); 53 | private const HASH_ADD_NEXT = (1 << 4); 54 | 55 | private CData $pointer; 56 | 57 | public function __construct(CData $hashInstance) 58 | { 59 | $this->pointer = $hashInstance; 60 | } 61 | 62 | /** 63 | * Retrieve an external iterator 64 | * 65 | * @return Traversable An instance of an object implementing Iterator or Traversable 66 | */ 67 | public function getIterator() 68 | { 69 | $iterator = function () { 70 | $index = 0; 71 | while ($index < $this->pointer->nNumOfElements) { 72 | $item = $this->pointer->arData[$index]; 73 | $index++; 74 | if ($item->val->u1->v->type === ReflectionValue::IS_UNDEF) { 75 | continue; 76 | } 77 | $key = $item->key !== null ? StringEntry::fromCData($item->key)->getStringValue() : null; 78 | yield $key => ReflectionValue::fromValueEntry($item->val); 79 | } 80 | }; 81 | 82 | return $iterator(); 83 | } 84 | 85 | /** 86 | * Performs search by key in the hashtable 87 | * 88 | * @param string $key Key to find 89 | * 90 | * @return ReflectionValue|null Value or null if not found 91 | */ 92 | public function find(string $key): ?ReflectionValue 93 | { 94 | $stringEntry = new StringEntry($key); 95 | $pointer = Core::call('zend_hash_find', $this->pointer, Core::addr($stringEntry->getRawValue())); 96 | 97 | if ($pointer !== null) { 98 | $pointer = ReflectionValue::fromValueEntry($pointer); 99 | } 100 | 101 | return $pointer; 102 | } 103 | 104 | /** 105 | * Deletes a value by key from the hashtable 106 | * 107 | * @param string $key Key in the hash to delete 108 | * @internal 109 | */ 110 | public function delete(string $key): void 111 | { 112 | $stringEntry = new StringEntry($key); 113 | $result = Core::call('zend_hash_del', $this->pointer, Core::addr($stringEntry->getRawValue())); 114 | if ($result === Core::FAILURE) { 115 | throw new \RuntimeException("Can not delete an item with key {$key}"); 116 | } 117 | } 118 | 119 | /** 120 | * Adds new value to the HashTable 121 | */ 122 | public function add(string $key, ReflectionValue $value): void 123 | { 124 | $stringEntry = new StringEntry($key); 125 | $result = Core::call( 126 | 'zend_hash_add_or_update', 127 | $this->pointer, 128 | Core::addr($stringEntry->getRawValue()), 129 | $value->getRawValue(), 130 | self::HASH_ADD_NEW 131 | ); 132 | if ($result === Core::FAILURE) { 133 | throw new \RuntimeException("Can not add an item with key {$key}"); 134 | } 135 | } 136 | 137 | public function __debugInfo() 138 | { 139 | return iterator_to_array($this->getIterator()); 140 | } 141 | 142 | /** 143 | * This method should return an instance of zend_refcounted_h 144 | */ 145 | protected function getGC(): CData 146 | { 147 | return $this->pointer->gc; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionClassConstant.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use FFI\CData; 16 | use ReflectionClassConstant as NativeReflectionClassConstant; 17 | use ZEngine\Core; 18 | use ZEngine\Type\HashTable; 19 | use ZEngine\Type\StringEntry; 20 | 21 | /** 22 | * Class ReflectionClassConstant 23 | * 24 | * typedef struct _zend_class_constant { 25 | * zval value; // access flags are stored in reserved: zval.u2.access_flags 26 | * zend_string *doc_comment; 27 | * HashTable *attributes; 28 | * zend_class_entry *ce; 29 | * } zend_class_constant; 30 | */ 31 | class ReflectionClassConstant extends NativeReflectionClassConstant 32 | { 33 | private CData $pointer; 34 | 35 | public function __construct(string $className, string $constantName) 36 | { 37 | parent::__construct($className, $constantName); 38 | 39 | $normalizedName = strtolower($className); 40 | $classEntryValue = Core::$executor->classTable->find($normalizedName); 41 | if ($classEntryValue === null) { 42 | throw new \ReflectionException("Class {$className} should be in the engine."); 43 | } 44 | $classEntry = $classEntryValue->getRawClass(); 45 | $constantsTable = new HashTable(Core::addr($classEntry->constants_table)); 46 | 47 | $constantEntry = $constantsTable->find($constantName); 48 | if ($constantEntry === null) { 49 | throw new \ReflectionException("Constant {$constantName} was not found in the class."); 50 | } 51 | $constantPointer = $constantEntry->getRawPointer(); 52 | $this->pointer = Core::cast('zend_class_constant *', $constantPointer); 53 | } 54 | 55 | /** 56 | * Creates a reflection from the zend_class_constant structure 57 | * 58 | * @param CData $constantEntry Pointer to the structure 59 | * 60 | * @return ReflectionClassConstant 61 | */ 62 | public static function fromCData(CData $constantEntry, string $constantName): ReflectionClassConstant 63 | { 64 | /** @var ReflectionClassConstant $reflectionConstant */ 65 | $reflectionConstant = (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); 66 | $className = StringEntry::fromCData($constantEntry->ce->name); 67 | call_user_func( 68 | [$reflectionConstant, 'parent::__construct'], 69 | $className->getStringValue(), 70 | $constantName 71 | ); 72 | $reflectionConstant->pointer = $constantEntry; 73 | 74 | return $reflectionConstant; 75 | } 76 | 77 | /** 78 | * Declares constant as public 79 | */ 80 | public function setPublic(): void 81 | { 82 | $this->pointer->value->u2->access_flags &= (~Core::ZEND_ACC_PPP_MASK); 83 | $this->pointer->value->u2->access_flags |= Core::ZEND_ACC_PUBLIC; 84 | } 85 | 86 | /** 87 | * Declares constant as protected 88 | */ 89 | public function setProtected(): void 90 | { 91 | $this->pointer->value->u2->access_flags &= (~Core::ZEND_ACC_PPP_MASK); 92 | $this->pointer->value->u2->access_flags |= Core::ZEND_ACC_PROTECTED; 93 | } 94 | 95 | /** 96 | * Declares constant as private 97 | */ 98 | public function setPrivate(): void 99 | { 100 | $this->pointer->value->u2->access_flags &= (~Core::ZEND_ACC_PPP_MASK); 101 | $this->pointer->value->u2->access_flags |= Core::ZEND_ACC_PRIVATE; 102 | } 103 | 104 | /** 105 | * Gets the declaring class 106 | */ 107 | public function getDeclaringClass(): ReflectionClass 108 | { 109 | return ReflectionClass::fromCData($this->pointer->ce); 110 | } 111 | 112 | /** 113 | * Changes the declaring class name for this property 114 | * 115 | * @param string $className New class name for this property 116 | * @internal 117 | */ 118 | public function setDeclaringClass(string $className): void 119 | { 120 | $lcName = strtolower($className); 121 | 122 | $classEntryValue = Core::$executor->classTable->find($lcName); 123 | if ($classEntryValue === null) { 124 | throw new \ReflectionException("Class {$className} was not found"); 125 | } 126 | $this->pointer->ce = $classEntryValue->getRawClass(); 127 | } 128 | 129 | /** 130 | * Returns a reflection value for this constant 131 | */ 132 | public function getReflectionValue(): ReflectionValue 133 | { 134 | return ReflectionValue::fromValueEntry($this->pointer->value); 135 | } 136 | 137 | /** 138 | * Returns a user-friendly representation of internal structure to prevent segfault 139 | */ 140 | public function __debugInfo(): array 141 | { 142 | return [ 143 | 'name' => $this->getName(), 144 | 'class' => $this->getDeclaringClass()->getName(), 145 | ]; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionProperty.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use FFI\CData; 16 | use ReflectionProperty as NativeReflectionProperty; 17 | use ZEngine\Core; 18 | use ZEngine\Type\HashTable; 19 | use ZEngine\Type\StringEntry; 20 | 21 | /** 22 | * Class ReflectionProperty 23 | * 24 | * typedef struct _zend_property_info { 25 | * uint32_t offset; // property offset for object properties or property index for static properties 26 | * uint32_t flags; 27 | * zend_string *name; 28 | * zend_string *doc_comment; 29 | * zend_class_entry *ce; 30 | * zend_type type; 31 | * } zend_property_info; 32 | */ 33 | class ReflectionProperty extends NativeReflectionProperty 34 | { 35 | private CData $pointer; 36 | 37 | public function __construct(string $className, string $propertyName) 38 | { 39 | parent::__construct($className, $propertyName); 40 | 41 | $normalizedName = strtolower($className); 42 | $classEntryValue = Core::$executor->classTable->find($normalizedName); 43 | if ($classEntryValue === null) { 44 | throw new \ReflectionException("Class {$className} should be in the engine."); 45 | } 46 | $classEntry = $classEntryValue->getRawClass(); 47 | $propertiesTable = new HashTable(Core::addr($classEntry->properties_info)); 48 | 49 | $propertyEntry = $propertiesTable->find(strtolower($propertyName)); 50 | if ($propertyEntry === null) { 51 | throw new \ReflectionException("Property {$propertyName} was not found in the class."); 52 | } 53 | $propertyPointer = $propertyEntry->getRawPointer(); 54 | $this->pointer = Core::cast('zend_property_info *', $propertyPointer); 55 | } 56 | 57 | /** 58 | * Creates a reflection from the zend_property_info structure 59 | * 60 | * @param CData $propertyEntry Pointer to the structure 61 | */ 62 | public static function fromCData(CData $propertyEntry): ReflectionProperty 63 | { 64 | /** @var ReflectionProperty $reflectionProperty */ 65 | $reflectionProperty = (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); 66 | $propertyName = StringEntry::fromCData($propertyEntry->name); 67 | call_user_func( 68 | [$reflectionProperty, 'parent::__construct'], 69 | $propertyName->getStringValue() 70 | ); 71 | $reflectionProperty->pointer = $propertyEntry; 72 | 73 | return $reflectionProperty; 74 | } 75 | 76 | /** 77 | * Returns an offset of this property 78 | */ 79 | public function getOffset(): int 80 | { 81 | return $this->pointer->offset; 82 | } 83 | 84 | /** 85 | * Declares property as public 86 | */ 87 | public function setPublic(): void 88 | { 89 | $this->pointer->flags &= (~Core::ZEND_ACC_PPP_MASK); 90 | $this->pointer->flags |= Core::ZEND_ACC_PUBLIC; 91 | } 92 | 93 | /** 94 | * Declares property as protected 95 | */ 96 | public function setProtected(): void 97 | { 98 | $this->pointer->flags &= (~Core::ZEND_ACC_PPP_MASK); 99 | $this->pointer->flags |= Core::ZEND_ACC_PROTECTED; 100 | } 101 | 102 | /** 103 | * Declares property as private 104 | */ 105 | public function setPrivate(): void 106 | { 107 | $this->pointer->flags &= (~Core::ZEND_ACC_PPP_MASK); 108 | $this->pointer->flags |= Core::ZEND_ACC_PRIVATE; 109 | } 110 | 111 | /** 112 | * Declares property as static/non-static 113 | */ 114 | public function setStatic(bool $isStatic = true): void 115 | { 116 | if ($isStatic) { 117 | $this->pointer->flags |= Core::ZEND_ACC_STATIC; 118 | } else { 119 | $this->pointer->flags &= (~Core::ZEND_ACC_STATIC); 120 | } 121 | } 122 | 123 | /** 124 | * Gets the declaring class 125 | */ 126 | public function getDeclaringClass(): ReflectionClass 127 | { 128 | return ReflectionClass::fromCData($this->pointer->ce); 129 | } 130 | 131 | /** 132 | * Changes the declaring class name for this property 133 | * 134 | * @param string $className New class name for this property 135 | * @internal 136 | */ 137 | public function setDeclaringClass(string $className): void 138 | { 139 | $lcName = strtolower($className); 140 | 141 | $classEntryValue = Core::$executor->classTable->find($lcName); 142 | if ($classEntryValue === null) { 143 | throw new \ReflectionException("Class {$className} was not found"); 144 | } 145 | $this->pointer->ce = $classEntryValue->getRawClass(); 146 | } 147 | 148 | /** 149 | * Returns a user-friendly representation of internal structure to prevent segfault 150 | */ 151 | public function __debugInfo(): array 152 | { 153 | return [ 154 | 'name' => $this->getName(), 155 | 'offset' => $this->getOffset(), 156 | 'type' => $this->getType(), 157 | 'class' => $this->getDeclaringClass()->getName() 158 | ]; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tests/Reflection/ReflectionValueTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use FFI\CData; 16 | use PHPUnit\Framework\TestCase; 17 | use ZEngine\Core; 18 | use ZEngine\Type\ObjectEntry; 19 | use ZEngine\Type\StringEntry; 20 | 21 | class ReflectionValueTest extends TestCase 22 | { 23 | /** 24 | * @dataProvider valueTypeProvider 25 | */ 26 | public function testConstructorWorks($value, int $expectedType) 27 | { 28 | $refValue = new ReflectionValue($value); 29 | $type = $refValue->getType() & 0xFF; 30 | 31 | $this->assertSame($expectedType, $type); 32 | } 33 | 34 | /** 35 | * @dataProvider valueProvider 36 | */ 37 | public function testGetNativeValue($value): void 38 | { 39 | // This prevents optimization of opcodes and $value variable GC 40 | static $currentValue; 41 | $currentValue = $value; 42 | $argument = Core::$executor->getExecutionState()->getArgument(0); 43 | $argument->getNativeValue($returnedValue); 44 | $this->assertSame($currentValue, $returnedValue); 45 | } 46 | 47 | public function valueProvider(): array 48 | { 49 | return [ 50 | [1], 51 | [1.0], 52 | ['Test'], 53 | [new \stdClass()], 54 | [[1, 2, 3]] 55 | ]; 56 | } 57 | 58 | /** 59 | * @dataProvider valueTypeProvider 60 | */ 61 | public function testGetType($value, int $expectedType): void 62 | { 63 | $argument = Core::$executor->getExecutionState()->getArgument(0); 64 | $argType = ($argument->getType() & 0xFF); // Use only low byte to get type name 65 | $expectedTypeName = ReflectionValue::name($expectedType); 66 | $argTypeName = ReflectionValue::name($argType); 67 | $this->assertSame( 68 | $expectedType, 69 | $argType, 70 | "Expect type to be ". $expectedTypeName . ', but ' . $argTypeName . ' given.' 71 | ); 72 | } 73 | 74 | public function valueTypeProvider(): array 75 | { 76 | $valueByRef = new \stdClass(); 77 | return [ 78 | [1, ReflectionValue::IS_LONG], 79 | [1.0, ReflectionValue::IS_DOUBLE], 80 | ['Test', ReflectionValue::IS_STRING], 81 | [new \stdClass(), ReflectionValue::IS_OBJECT], 82 | [[1, 2, 3], ReflectionValue::IS_ARRAY], 83 | [null, ReflectionValue::IS_NULL], 84 | [false, ReflectionValue::IS_FALSE], 85 | [true, ReflectionValue::IS_TRUE], 86 | [fopen(__FILE__, 'r'), ReflectionValue::IS_RESOURCE] 87 | ]; 88 | } 89 | 90 | public function testGetRawClass() 91 | { 92 | $classEntry = Core::$executor->classTable->find(strtolower(self::class)); 93 | $rawClass = $classEntry->getRawClass(); 94 | $this->assertInstanceOf(CData::class, $rawClass); 95 | 96 | // Let's check the name from this structure 97 | $className = StringEntry::fromCData($rawClass->name); 98 | $this->assertSame(self::class, $className->getStringValue()); 99 | } 100 | 101 | public function testGetRawFunction() 102 | { 103 | $functionEntry = Core::$executor->functionTable->find('var_dump'); 104 | $rawFunction = $functionEntry->getRawFunction(); 105 | $this->assertInstanceOf(CData::class, $rawFunction); 106 | 107 | // Let's check the name from this structure 108 | $functionName = StringEntry::fromCData($rawFunction->function_name); 109 | $this->assertSame('var_dump', $functionName->getStringValue()); 110 | } 111 | 112 | public function testGetRawValue() 113 | { 114 | $classEntry = Core::$executor->classTable->find(strtolower(self::class)); 115 | $rawValue = $classEntry->getRawValue(); 116 | 117 | $valueEntry = ReflectionValue::fromValueEntry($rawValue); 118 | $this->assertEquals(ReflectionValue::IS_PTR, $valueEntry->getType()); 119 | } 120 | 121 | public function testSetNativeValue() 122 | { 123 | $this->markTestSkipped('Can not construct ReflectionValue by hand now'); 124 | } 125 | 126 | public function testGetRawObject() 127 | { 128 | $thisValue = Core::$executor->getExecutionState()->getThis(); 129 | $rawObject = $thisValue->getRawObject(); 130 | $this->assertInstanceOf(CData::class, $rawObject); 131 | 132 | $object = ObjectEntry::fromCData($rawObject); 133 | // Check that we have the same object by checking handle 134 | $this->assertSame(spl_object_id($this), $object->getHandle()); 135 | } 136 | 137 | public function testGetRawString() 138 | { 139 | $value = self::class; 140 | get_defined_vars(); // This triggers Symbol Table rebuilt under the hood 141 | 142 | $valueEntry = Core::$executor->getExecutionState()->getSymbolTable()->find('value'); 143 | 144 | // We know that $valueEntry is indirect pointer to string 145 | $this->assertSame(ReflectionValue::IS_INDIRECT, $valueEntry->getType()); 146 | $rawString = $valueEntry->getIndirectValue()->getRawString(); 147 | 148 | $stringEntry = StringEntry::fromCData($rawString); 149 | $this->assertSame(self::class, $stringEntry->getStringValue()); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/EngineExtension/AbstractModule.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\EngineExtension; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\EngineExtension\Hook\ExtensionConstructorHook; 18 | use ZEngine\Reflection\ReflectionExtension; 19 | 20 | abstract class AbstractModule extends ReflectionExtension implements ModuleInterface 21 | { 22 | /** 23 | * @see zend_modules.h:MODULE_PERSISTENT 24 | */ 25 | private const MODULE_PERSISTENT = 1; 26 | 27 | /** 28 | * @see zend_modules.h:MODULE_TEMPORARY 29 | */ 30 | private const MODULE_TEMPORARY = 2; 31 | 32 | /** 33 | * Unique name of this module 34 | */ 35 | private string $moduleName; 36 | 37 | /** 38 | * Module constructor. 39 | * 40 | * @param string|null $moduleName Module name (optional). If not set, class name will be used as module name 41 | */ 42 | final public function __construct(string $moduleName = null) 43 | { 44 | $this->moduleName = $moduleName ?? self::detectModuleName(); 45 | 46 | // if module is already registered, then we can use it immediately 47 | if ($this->isModuleRegistered()) { 48 | parent::__construct($this->moduleName); 49 | } 50 | } 51 | 52 | /** 53 | * Returns the unique name of this module 54 | */ 55 | final public function getName(): string 56 | { 57 | return $this->moduleName; 58 | } 59 | 60 | /** 61 | * Checks if this module loaded or not 62 | */ 63 | final public function isModuleRegistered(): bool 64 | { 65 | return extension_loaded($this->moduleName); 66 | } 67 | 68 | /** 69 | * Performs registration of this module in the engine 70 | */ 71 | final public function register(): void 72 | { 73 | if ($this->isModuleRegistered()) { 74 | throw new \RuntimeException('Module ' . $this->moduleName . ' was already registered.'); 75 | } 76 | 77 | // We don't need persistent memory here, as PHP copies structures into persistent memory itself 78 | $module = Core::new('zend_module_entry'); 79 | $moduleName = $this->moduleName; 80 | $nameLength = strlen($moduleName) + 1; /* extra zero-byte */; 81 | $rawName = Core::new("char[$nameLength]", false, static::targetPersistent()); 82 | Core::memcpy($rawName, $moduleName, $nameLength - 1); 83 | $rawName[$nameLength - 1] = "\0"; 84 | 85 | $module->size = Core::sizeof($module); 86 | $module->type = static::targetPersistent() ? self::MODULE_PERSISTENT : self::MODULE_TEMPORARY; 87 | $module->name = $rawName; 88 | $module->zend_api = static::targetApiVersion(); 89 | $module->zend_debug = (int)static::targetDebug(); 90 | $module->zts = (int)static::targetThreadSafe(); 91 | 92 | $globalType = static::globalType(); 93 | if ($globalType !== null) { 94 | $module->globals_size = Core::sizeof(Core::type($globalType)); 95 | $memoryStructure = Core::new($globalType, false, static::targetPersistent()); 96 | $module->globals_ptr = Core::addr($memoryStructure); 97 | } 98 | 99 | // $module pointer will be updated, as registration method returns a copy of memory 100 | $realModulePointer = Core::call('zend_register_module_ex', Core::addr($module)); 101 | 102 | $this->moduleEntry = $realModulePointer; 103 | 104 | $extensionConstructor = \ReflectionExtension::class . '::__construct'; 105 | call_user_func([$this, $extensionConstructor], $moduleName); 106 | } 107 | 108 | /** 109 | * Starts this module 110 | * 111 | * Startup includes calling callbacks for global memory allocation, checking deps, etc 112 | */ 113 | final public function startup(): void 114 | { 115 | if ($this instanceof ControlModuleGlobalsInterface) { 116 | $closure = (new \ReflectionMethod($this, '__globalConstruct'))->getClosure(); 117 | $hook = new ExtensionConstructorHook($closure, $this->moduleEntry); 118 | $hook->install(); 119 | } 120 | 121 | $result = Core::call('zend_startup_module_ex', $this->moduleEntry); 122 | if ($result !== Core::SUCCESS) { 123 | throw new \RuntimeException('Can not startup module ' . $this->moduleName); 124 | } 125 | } 126 | 127 | /** 128 | * This getter extends general logic with automatic casting global memory to required type 129 | * 130 | * @inheritDoc 131 | */ 132 | final public function getGlobals(): ?CData 133 | { 134 | $rawPointer = parent::getGlobals(); 135 | if ($rawPointer !== null) { 136 | $rawPointer = Core::cast(static::globalType(), $rawPointer); 137 | } 138 | 139 | return $rawPointer; 140 | } 141 | 142 | /** 143 | * Detects a module name by class name 144 | */ 145 | private static function detectModuleName(): string 146 | { 147 | $classNameParts = explode('\\', static::class); 148 | $className = end($classNameParts); 149 | $prefixName = strstr($className, 'Module', true); 150 | if ($prefixName !== false) { 151 | $className = $prefixName; 152 | } 153 | // Converts camelCase to snake_case 154 | $moduleName = strtolower(preg_replace_callback('/([a-z])([A-Z])/', function ($match) { 155 | return $match[1] . '_' . $match[2]; 156 | }, $className)); 157 | 158 | return $moduleName; 159 | } 160 | } -------------------------------------------------------------------------------- /src/Reflection/ReflectionExtension.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use FFI\CData; 16 | use ReflectionClass as NativeReflectionClass; 17 | use ReflectionExtension as NativeReflectionExtension; 18 | use ZEngine\Core; 19 | 20 | /** 21 | * Class ReflectionExtension 22 | * 23 | * struct _zend_module_entry { 24 | * unsigned short size; 25 | * unsigned int zend_api; 26 | * unsigned char zend_debug; 27 | * unsigned char zts; 28 | * const struct _zend_ini_entry *ini_entry; 29 | * const struct _zend_module_dep *deps; 30 | * const char *name; 31 | * const struct _zend_function_entry *functions; 32 | * int (*module_startup_func)(int type, int module_number); 33 | * int (*module_shutdown_func)(int type, int module_number); 34 | * int (*request_startup_func)(int type, int module_number); 35 | * int (*request_shutdown_func)(int type, int module_number); 36 | * void (*info_func)(zend_module_entry *zend_module); 37 | * const char *version; 38 | * size_t globals_size; 39 | * #ifdef ZTS 40 | * ts_rsrc_id* globals_id_ptr; 41 | * #else 42 | * void* globals_ptr; 43 | * #endif 44 | * void (*globals_ctor)(void *global); 45 | * void (*globals_dtor)(void *global); 46 | * int (*post_deactivate_func)(void); 47 | * int module_started; 48 | * unsigned char type; 49 | * void *handle; 50 | * int module_number; 51 | * const char *build_id; 52 | * }; 53 | */ 54 | class ReflectionExtension extends NativeReflectionExtension 55 | { 56 | protected CData $moduleEntry; 57 | 58 | /** 59 | * @inheritDoc 60 | */ 61 | public function __construct(string $name) 62 | { 63 | parent::__construct($name); 64 | 65 | $moduleEntry = Core::$modules->find($name); 66 | if ($moduleEntry === null) { 67 | throw new \ReflectionException("Module {$name} should be in the engine."); 68 | } 69 | $rawPointer = $moduleEntry->getRawPointer(); 70 | $this->moduleEntry = Core::cast('zend_module_entry *', $rawPointer); 71 | } 72 | 73 | /** 74 | * Creates an instance of extension from a low-level data structure 75 | * 76 | * @param CData $moduleEntry Pointer to the `zend_module_entry` structure 77 | */ 78 | public static function fromCData(CData $moduleEntry): self 79 | { 80 | /** @var self $extension */ 81 | $extension = (new NativeReflectionClass(self::class))->newInstanceWithoutConstructor(); 82 | $extension->moduleEntry = $moduleEntry; 83 | 84 | call_user_func([$extension, 'parent::__construct'], $moduleEntry->name); 85 | 86 | return $extension; 87 | } 88 | 89 | /** 90 | * Returns the size of module itself 91 | * 92 | * Typically, this should be equal to Core::type('zend_module_entry') 93 | */ 94 | public function getSize(): int 95 | { 96 | return $this->moduleEntry->size; 97 | } 98 | 99 | /** 100 | * Returns the size of module global structure 101 | */ 102 | public function getGlobalsSize(): int 103 | { 104 | return $this->moduleEntry->globals_size; 105 | } 106 | 107 | /** 108 | * Returns a pointer (if any) to global memory area or null if extension doesn't use global memory structure 109 | */ 110 | public function getGlobals(): ?CData 111 | { 112 | return $this->moduleEntry->globals_ptr; 113 | } 114 | 115 | /** 116 | * Was module started or not 117 | */ 118 | public function wasModuleStarted(): bool 119 | { 120 | return (bool) $this->moduleEntry->module_started; 121 | } 122 | 123 | /** 124 | * Is module was compiled/designed for debug mode 125 | * 126 | * @see ZEND_DEBUG_BUILD 127 | */ 128 | public function isDebug(): bool 129 | { 130 | return (bool) $this->moduleEntry->zend_debug; 131 | } 132 | 133 | /** 134 | * Is module compiled with thread safety or not 135 | * 136 | * @see ZEND_THREAD_SAFE 137 | */ 138 | public function isThreadSafe(): bool 139 | { 140 | return (bool) $this->moduleEntry->zts; 141 | } 142 | 143 | /** 144 | * Returns the module ordinal number 145 | */ 146 | public function getModuleNumber(): int 147 | { 148 | return $this->moduleEntry->module_number; 149 | } 150 | 151 | /** 152 | * Returns the api version 153 | */ 154 | public function getApiVersion(): int 155 | { 156 | return $this->moduleEntry->zend_api; 157 | } 158 | 159 | /** 160 | * This method is used to prevent segmentation faults when dumping CData 161 | */ 162 | public function __debugInfo() 163 | { 164 | if (!isset($this->moduleEntry)) { 165 | return []; 166 | } 167 | $result = []; 168 | $methods = (new NativeReflectionClass(self::class))->getMethods(ReflectionMethod::IS_PUBLIC); 169 | foreach ($methods as $method) { 170 | $methodName = $method->getName(); 171 | $hasZeroArgs = $method->getNumberOfRequiredParameters() === 0; 172 | if ((strpos($methodName, 'get') === 0) && $hasZeroArgs) { 173 | $friendlyName = lcfirst(substr($methodName, 3)); 174 | $result[$friendlyName] = $this->$methodName(); 175 | } 176 | if ((strpos($methodName, 'is') === 0) && $hasZeroArgs) { 177 | $friendlyName = lcfirst(substr($methodName, 2)); 178 | $result[$friendlyName] = $this->$methodName(); 179 | } 180 | } 181 | 182 | return $result; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/AbstractSyntaxTree/DeclarationNode.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\AbstractSyntaxTree; 14 | 15 | use ZEngine\Core; 16 | use ZEngine\Type\StringEntry; 17 | 18 | /** 19 | * DeclarationNode is used for class and function declarations 20 | * 21 | * typedef struct _zend_ast_decl { 22 | * zend_ast_kind kind; 23 | * zend_ast_attr attr; // Unused - for structure compatibility 24 | * uint32_t start_lineno; 25 | * uint32_t end_lineno; 26 | * uint32_t flags; 27 | * unsigned char *lex_pos; 28 | * zend_string *doc_comment; 29 | * zend_string *name; 30 | * zend_ast *child[4]; 31 | * } zend_ast_decl; 32 | */ 33 | class DeclarationNode extends Node 34 | { 35 | /** 36 | * Creates a declaration of given type 37 | */ 38 | public function __construct( 39 | int $kind, 40 | int $flags, 41 | int $startLine, 42 | int $endLine, 43 | string $docComment, 44 | string $name, 45 | ?NodeInterface ...$childrenNodes 46 | ) { 47 | if (!NodeKind::isSpecial($kind)) { 48 | $kindName = NodeKind::name($kind); 49 | throw new \InvalidArgumentException('Given AST type ' . $kindName . ' does not belong to declaration'); 50 | } 51 | 52 | if (count($childrenNodes) > 4) { 53 | throw new \InvalidArgumentException('Declaration node can contain only up to 4 children nodes'); 54 | } 55 | 56 | // Fill exactly 4 nodes with default null values 57 | $childrenNodes = $childrenNodes + array_fill(0, 4, null); 58 | 59 | // ZEND_API zend_ast *zend_ast_create_decl( 60 | // zend_ast_kind kind, uint32_t flags, uint32_t start_lineno, zend_string *doc_comment, 61 | // zend_string *name, zend_ast *child0, zend_ast *child1, zend_ast *child2, zend_ast *child3 62 | //); 63 | $ast = Core::call( 64 | 'zend_ast_create_decl', $kind, $flags, $startLine, $endLine, $docComment, 65 | $name, ...$childrenNodes 66 | ); 67 | 68 | $declaration = Core::cast('zend_ast_decl *', $ast); 69 | 70 | $this->node = $declaration; 71 | } 72 | 73 | /** 74 | * As declaration node spans several lines, just return start line instead 75 | */ 76 | public function getLine(): int 77 | { 78 | return $this->node->start_lineno; 79 | } 80 | 81 | /** 82 | * Changes the node line (actually, it's a start line) 83 | */ 84 | public function setLine(int $newLine): void 85 | { 86 | $this->node->start_lineno = $newLine; 87 | } 88 | 89 | /** 90 | * Returns the end line 91 | */ 92 | public function getEndLine(): int 93 | { 94 | return $this->node->end_lineno; 95 | } 96 | 97 | /** 98 | * Changes the node end line 99 | */ 100 | public function setEndLine(int $newLine): void 101 | { 102 | $this->node->end_lineno = $newLine; 103 | } 104 | 105 | /** 106 | * Returns node flags 107 | */ 108 | public function getFlags(): int 109 | { 110 | return $this->node->flags; 111 | } 112 | 113 | /** 114 | * Changes node flags 115 | */ 116 | public function setFlags(int $newFlags): void 117 | { 118 | $this->node->flags = $newFlags; 119 | } 120 | 121 | /** 122 | * Returns node flags 123 | */ 124 | public function getLexPosition(): int 125 | { 126 | return $this->node->lex_pos[0]; 127 | } 128 | 129 | /** 130 | * Returns doc comment 131 | */ 132 | public function getDocComment(): string 133 | { 134 | if ($this->node->doc_comment === null) { 135 | return ''; 136 | } 137 | 138 | // TODO: investigate what to do with string copying 139 | return StringEntry::fromCData($this->node->doc_comment)->copy()->getStringValue(); 140 | } 141 | 142 | /** 143 | * Changes the doc comment for this declaration 144 | */ 145 | public function setDocComment(string $newDocComment): void 146 | { 147 | $entry = new StringEntry($newDocComment); 148 | 149 | // TODO: investigate what to do with string copying 150 | $this->node->doc_comment = $entry->copy()->getRawValue(); 151 | } 152 | 153 | /** 154 | * Returns the name of entry 155 | */ 156 | public function getName(): string 157 | { 158 | // TODO: investigate what to do with string copying 159 | return StringEntry::fromCData($this->node->name)->copy()->getStringValue(); 160 | } 161 | 162 | /** 163 | * Changes the name of this node 164 | */ 165 | public function setName(string $newName): void 166 | { 167 | $entry = new StringEntry($newName); 168 | 169 | // TODO: investigate what to do with string copying 170 | $this->node->name = $entry->copy()->getRawValue(); 171 | } 172 | 173 | /** 174 | * @inheritDoc 175 | */ 176 | public function getChildrenCount(): int 177 | { 178 | // Declaration node always contain 4 children nodes. 179 | return 4; 180 | } 181 | 182 | /** 183 | * @inheritDoc 184 | */ 185 | protected function dumpThis(int $indent = 0): string 186 | { 187 | $line = parent::dumpThis($indent); 188 | 189 | $kind = $this->getKind(); 190 | 191 | if ($kind !== NodeKind::AST_CLOSURE) { 192 | $line .= ' ' . $this->getName(); 193 | } 194 | 195 | $flags = $this->getFlags(); 196 | if ($flags !== 0) { 197 | $line .= sprintf(" flags(%04x)", $flags); 198 | } 199 | 200 | return $line; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /tests/System/ExecutionDataTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\System; 14 | 15 | use FFI\CData; 16 | use PHPUnit\Framework\TestCase; 17 | use ZEngine\Core; 18 | use ZEngine\Reflection\ReflectionValue; 19 | use ZEngine\Type\OpLine; 20 | 21 | class ExecutionDataTest extends TestCase 22 | { 23 | public function testHasPrevious() 24 | { 25 | $hasPrevious = Core::$executor->getExecutionState()->hasPrevious(); 26 | // This method is definitely called from PHPUnit, so it MUST contain previous entries 27 | $this->assertTrue($hasPrevious, 'This method is called from PHPUnit, it MUST contain previous entries'); 28 | } 29 | 30 | public function testGetPrevious() 31 | { 32 | $executionData = Core::$executor->getExecutionState()->getPrevious(); 33 | $this->assertInstanceOf(ExecutionData::class, $executionData); 34 | 35 | // We can compare our result with backtrace generated by debug_backtrace 36 | $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 37 | $this->assertSame($trace[1]['function'], $executionData->getFunction()->getName()); 38 | } 39 | 40 | /** 41 | * @group internal 42 | */ 43 | public function testGetSymbolTable() 44 | { 45 | $symTable = Core::$executor->getExecutionState()->getSymbolTable(); 46 | $this->assertNotNull($symTable); 47 | $this->markTestIncomplete('Segfaults if we try to look for local variables'); 48 | } 49 | 50 | /** 51 | * This test is tricky one to understand: PHP engine allocates a stack frame, where each variable is stored in 52 | * the stack frame. Thus, $a variable will be stored in first slot in the stack frame and we can access it to check 53 | */ 54 | public function testGetCallVariableByNumber() 55 | { 56 | // Do not use constants here to prevent opcode optimization and inlining 57 | $expected = microtime(true); 58 | 59 | // $expected will be the first temporary variable in the stack, so index will be 0 60 | $value = Core::$executor->getExecutionState()->getCallVariableByNumber(0); 61 | $this->assertInstanceOf(CData::class, $value); 62 | 63 | // Let's check if this value equals to our original $expected 64 | ReflectionValue::fromValueEntry($value)->getNativeValue($return); 65 | $this->assertNotNull($return); 66 | $this->assertSame($expected, $return); 67 | } 68 | 69 | /** 70 | * @dataProvider argumentProvider 71 | */ 72 | public function testGetArguments($arg1 = null, $arg2 = null, $arg3 = null) 73 | { 74 | $engineArguments = Core::$executor->getExecutionState()->getArguments(); 75 | $receivedArguments = []; 76 | foreach ($engineArguments as $reflectionValue) { 77 | // We can collect original PHP values from the Core one-by-one 78 | $reflectionValue->getNativeValue($value); 79 | $receivedArguments[] = $value; 80 | unset($value); 81 | } 82 | $this->assertEquals(func_get_args(), $receivedArguments); 83 | } 84 | 85 | /** 86 | * @dataProvider argumentProvider 87 | */ 88 | public function testGetNumberOfArguments() 89 | { 90 | $engineArguments = Core::$executor->getExecutionState()->getNumberOfArguments(); 91 | $expectedNumber = func_num_args(); 92 | $this->assertSame($expectedNumber, $engineArguments); 93 | } 94 | 95 | /** 96 | * @dataProvider argumentProvider 97 | */ 98 | public function testGetArgument($firstPhpArgument) 99 | { 100 | // Be aware, that getArgument() can return only declared arguments, not extra one! 101 | $firstArgument = Core::$executor->getExecutionState()->getArgument(0); 102 | 103 | $firstArgument->getNativeValue($firstEngineValue); 104 | $this->assertInstanceOf(ReflectionValue::class, $firstArgument); 105 | $this->assertSame($firstPhpArgument, $firstEngineValue); 106 | } 107 | 108 | public function argumentProvider(): array 109 | { 110 | return [ 111 | [1], 112 | ['a', false], 113 | [null, new \stdClass, 42.0] 114 | ]; 115 | } 116 | 117 | public function testGetOpline() 118 | { 119 | $opline = Core::$executor->getExecutionState()->getOpline(); 120 | $this->assertSame(__LINE__ - 1, $opline->getLine()); 121 | // we can't do more at the current stage, because code is already executed and we are not in opcode handler 122 | $this->assertInstanceOf(OpLine::class, $opline); 123 | } 124 | 125 | public function testGetThis() 126 | { 127 | $thisValue = Core::$executor->getExecutionState()->getThis(); 128 | $thisValue->getNativeValue($instance); 129 | $this->assertSame($this, $instance); 130 | 131 | // Just for fun: we can do crazy things like changing $this in current stack frame 132 | $self = $this; // Save current $this to call method on it later 133 | $thisValue->setNativeValue(new \stdClass); 134 | $self->assertInstanceOf(\stdClass::class, $this); 135 | } 136 | 137 | public function testGetFunction() 138 | { 139 | $reflectionFunction = Core::$executor->getExecutionState()->getFunction(); 140 | $this->assertSame(__FUNCTION__, $reflectionFunction->getName()); 141 | } 142 | 143 | public function testGetCallVariable() 144 | { 145 | $this->markTestSkipped('Very engine-specific method'); 146 | } 147 | 148 | public function testGetReturnValue() 149 | { 150 | $this->markTestSkipped('It is impossible to check this method, because return value will be overridden by PHP'); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionMethod.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use FFI\CData; 16 | use ReflectionMethod as NativeReflectionMethod; 17 | use ZEngine\Core; 18 | use ZEngine\Type\HashTable; 19 | use ZEngine\Type\StringEntry; 20 | 21 | class ReflectionMethod extends NativeReflectionMethod 22 | { 23 | use FunctionLikeTrait; 24 | 25 | public function __construct(string $className, string $methodName) 26 | { 27 | parent::__construct($className, $methodName); 28 | 29 | $normalizedName = strtolower($className); 30 | $classEntryValue = Core::$executor->classTable->find($normalizedName); 31 | if ($classEntryValue === null) { 32 | throw new \ReflectionException("Class {$className} should be in the engine."); 33 | } 34 | $classEntry = $classEntryValue->getRawClass(); 35 | $methodTable = new HashTable(Core::addr($classEntry->function_table)); 36 | 37 | $methodEntryValue = $methodTable->find(strtolower($methodName)); 38 | if ($methodEntryValue === null) { 39 | throw new \ReflectionException("Method {$methodName} was not found in the class."); 40 | } 41 | $this->pointer = $methodEntryValue->getRawFunction(); 42 | } 43 | 44 | /** 45 | * Creates a reflection from the zend_function/zend_internal_function structure 46 | * 47 | * @param CData $functionEntry Pointer to the structure 48 | * 49 | * @return ReflectionMethod 50 | */ 51 | public static function fromCData(CData $functionEntry): ReflectionMethod 52 | { 53 | /** @var ReflectionMethod $reflectionMethod */ 54 | $reflectionMethod = (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); 55 | if ($functionEntry->type !== Core::ZEND_INTERNAL_FUNCTION) { 56 | $functionNamePtr = $functionEntry->common->function_name; 57 | $scopeNamePtr = $functionEntry->common->scope->name; 58 | } else { 59 | $functionNamePtr = $functionEntry->function_name; 60 | $scopeNamePtr = $functionEntry->scope->name; 61 | } 62 | 63 | $scopeName = StringEntry::fromCData($scopeNamePtr); 64 | $functionName = StringEntry::fromCData($functionNamePtr); 65 | call_user_func( 66 | [$reflectionMethod, 'parent::__construct'], 67 | $scopeName->getStringValue(), 68 | $functionName->getStringValue() 69 | ); 70 | $reflectionMethod->pointer = $functionEntry; 71 | 72 | return $reflectionMethod; 73 | } 74 | 75 | /** 76 | * Declares function as final/non-final 77 | */ 78 | public function setFinal(bool $isFinal = true): void 79 | { 80 | if ($isFinal) { 81 | $this->getCommonPointer()->fn_flags |= Core::ZEND_ACC_FINAL; 82 | } else { 83 | $this->getCommonPointer()->fn_flags &= (~Core::ZEND_ACC_FINAL); 84 | } 85 | } 86 | 87 | /** 88 | * Declares function as abstract/non-abstract 89 | */ 90 | public function setAbstract(bool $isAbstract = true): void 91 | { 92 | if ($isAbstract) { 93 | $this->getCommonPointer()->fn_flags |= Core::ZEND_ACC_ABSTRACT; 94 | } else { 95 | $this->getCommonPointer()->fn_flags &= (~Core::ZEND_ACC_ABSTRACT); 96 | } 97 | } 98 | 99 | /** 100 | * Declares method as public 101 | */ 102 | public function setPublic(): void 103 | { 104 | $this->getCommonPointer()->fn_flags &= (~Core::ZEND_ACC_PPP_MASK); 105 | $this->getCommonPointer()->fn_flags |= Core::ZEND_ACC_PUBLIC; 106 | } 107 | 108 | /** 109 | * Declares method as protected 110 | */ 111 | public function setProtected(): void 112 | { 113 | $this->getCommonPointer()->fn_flags &= (~Core::ZEND_ACC_PPP_MASK); 114 | $this->getCommonPointer()->fn_flags |= Core::ZEND_ACC_PROTECTED; 115 | } 116 | 117 | /** 118 | * Declares method as private 119 | */ 120 | public function setPrivate(): void 121 | { 122 | $this->getCommonPointer()->fn_flags &= (~Core::ZEND_ACC_PPP_MASK); 123 | $this->getCommonPointer()->fn_flags |= Core::ZEND_ACC_PRIVATE; 124 | } 125 | 126 | /** 127 | * Declares method as static/non-static 128 | */ 129 | public function setStatic(bool $isStatic = true): void 130 | { 131 | if ($isStatic) { 132 | $this->getCommonPointer()->fn_flags |= Core::ZEND_ACC_STATIC; 133 | } else { 134 | $this->getCommonPointer()->fn_flags &= (~Core::ZEND_ACC_STATIC); 135 | } 136 | } 137 | 138 | /** 139 | * Gets the declaring class 140 | * 141 | * @throws \InvalidArgumentException If scope is not available 142 | */ 143 | public function getDeclaringClass(): ReflectionClass 144 | { 145 | if ($this->getCommonPointer()->scope === null) { 146 | throw new \InvalidArgumentException('Not in a class scope'); 147 | } 148 | 149 | return ReflectionClass::fromCData($this->getCommonPointer()->scope); 150 | } 151 | 152 | /** 153 | * Changes the declaring class name for this method 154 | * 155 | * @param string $className New class name for this method 156 | * @internal 157 | */ 158 | public function setDeclaringClass(string $className): void 159 | { 160 | $lcName = strtolower($className); 161 | 162 | $classEntryValue = Core::$executor->classTable->find($lcName); 163 | if ($classEntryValue === null) { 164 | throw new \ReflectionException("Class {$className} was not found"); 165 | } 166 | $this->getCommonPointer()->scope = $classEntryValue->getRawClass(); 167 | } 168 | 169 | /** 170 | * Returns the method prototype or null if no prototype for this method 171 | */ 172 | public function getPrototype(): ?ReflectionMethod 173 | { 174 | if ($this->getCommonPointer()->prototype === null) { 175 | return null; 176 | } 177 | 178 | return static::fromCData($this->getCommonPointer()->prototype); 179 | } 180 | 181 | /** 182 | * Returns a user-friendly representation of internal structure to prevent segfault 183 | */ 184 | public function __debugInfo(): array 185 | { 186 | return [ 187 | 'name' => $this->getName(), 188 | 'class' => $this->getDeclaringClass()->getName() 189 | ]; 190 | } 191 | 192 | /** 193 | * Returns the hash key for function or method 194 | */ 195 | protected function getHash(): string 196 | { 197 | return $this->class . '::' . $this->name; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/Reflection/ReflectionMethodTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Reflection; 14 | 15 | use Error; 16 | use PHPUnit\Framework\Error\Deprecated; 17 | use PHPUnit\Framework\TestCase; 18 | use ZEngine\Stub\TestClass; 19 | 20 | class ReflectionMethodTest extends TestCase 21 | { 22 | private ReflectionMethod $refMethod; 23 | 24 | protected function setUp(): void 25 | { 26 | $this->refMethod = new ReflectionMethod(TestClass::class, 'reflectedMethod'); 27 | } 28 | 29 | public function testSetFinal(): void 30 | { 31 | $this->refMethod->setFinal(true); 32 | $this->assertTrue($this->refMethod->isFinal()); 33 | 34 | // If we try to override this method now in child class, then E_COMPILE_ERROR will be raised 35 | } 36 | 37 | /** 38 | * @depends testSetFinal 39 | */ 40 | public function testSetNonFinal(): void 41 | { 42 | $this->refMethod->setFinal(false); 43 | $this->assertFalse($this->refMethod->isFinal()); 44 | } 45 | 46 | public function testSetAbstract(): void 47 | { 48 | $this->refMethod->setAbstract(true); 49 | $this->assertTrue($this->refMethod->isAbstract()); 50 | 51 | $this->expectException(Error::class); 52 | $this->expectExceptionMessage('Cannot call abstract method ZEngine\Stub\TestClass::reflectedMethod()'); 53 | $test = new TestClass(); 54 | $test->reflectedMethod(); 55 | } 56 | 57 | /** 58 | * @depends testSetAbstract 59 | */ 60 | public function testSetNonAbstract(): void 61 | { 62 | $this->refMethod->setAbstract(false); 63 | $this->assertFalse($this->refMethod->isAbstract()); 64 | // We expect no errors here 65 | $test = new TestClass(); 66 | $test->reflectedMethod(); 67 | } 68 | 69 | public function testSetPrivate(): void 70 | { 71 | $this->refMethod->setPrivate(); 72 | $this->assertTrue($this->refMethod->isPrivate()); 73 | $this->assertFalse($this->refMethod->isPublic()); 74 | $this->assertFalse($this->refMethod->isProtected()); 75 | 76 | $this->expectException(Error::class); 77 | $this->expectExceptionMessageMatches('/Call to private method .*?reflectedMethod()/'); 78 | $test = new TestClass(); 79 | $test->reflectedMethod(); 80 | } 81 | 82 | /** 83 | * @depends testSetPrivate 84 | */ 85 | public function testSetProtected(): void 86 | { 87 | $this->refMethod->setProtected(); 88 | $this->assertTrue($this->refMethod->isProtected()); 89 | $this->assertFalse($this->refMethod->isPrivate()); 90 | $this->assertFalse($this->refMethod->isPublic()); 91 | 92 | // We can override+call protected method from child by making it public 93 | $child = new class extends TestClass { 94 | public function reflectedMethod(): ?string 95 | { 96 | // call to the parent method which is protected now 97 | return parent::reflectedMethod(); 98 | } 99 | }; 100 | $child->reflectedMethod(); 101 | 102 | // If we call our protected method, we should have an error here 103 | $this->expectException(Error::class); 104 | $this->expectExceptionMessageMatches('/Call to protected method .*?reflectedMethod()/'); 105 | $test = new TestClass(); 106 | $test->reflectedMethod(); 107 | } 108 | 109 | /** 110 | * @depends testSetProtected 111 | */ 112 | public function testSetPublic(): void 113 | { 114 | $this->refMethod->setPublic(); 115 | $this->assertTrue($this->refMethod->isPublic()); 116 | $this->assertFalse($this->refMethod->isPrivate()); 117 | $this->assertFalse($this->refMethod->isProtected()); 118 | 119 | $test = new TestClass(); 120 | $result = $test->reflectedMethod(); 121 | $this->assertSame(TestClass::class, $result); 122 | } 123 | 124 | public function testSetStatic(): void 125 | { 126 | $this->refMethod->setStatic(); 127 | $this->assertTrue($this->refMethod->isStatic()); 128 | 129 | $test = new TestClass(); 130 | $result = $test->reflectedMethod(); 131 | 132 | // We call our method statically now, thus it should return null as class name 133 | $this->assertNull($result); 134 | } 135 | 136 | /** 137 | * @depends testSetStatic 138 | */ 139 | public function testSetNonStatic(): void 140 | { 141 | $this->refMethod->setStatic(false); 142 | $this->assertFalse($this->refMethod->isStatic()); 143 | } 144 | 145 | public function testSetDeprecated(): void 146 | { 147 | $this->refMethod->setDeprecated(); 148 | $this->assertTrue($this->refMethod->isDeprecated()); 149 | 150 | // $this->expectDeprecation(); 151 | // $this->expectDeprecationMessageMatches('/Function .*?reflectedMethod\(\) is deprecated/'); 152 | $test = new TestClass(); 153 | $test->reflectedMethod(); 154 | $this->markTestSkipped('User method does not trigger deprecation error'); 155 | } 156 | 157 | /** 158 | * @depends testSetDeprecated 159 | */ 160 | public function testSetNonDeprecated(): void 161 | { 162 | try { 163 | $currentReporting = error_reporting(); 164 | error_reporting(E_ALL); 165 | $this->refMethod->setDeprecated(false); 166 | $this->assertFalse($this->refMethod->isDeprecated()); 167 | 168 | // We expect no deprecation errors now 169 | $test = new TestClass(); 170 | $test->reflectedMethod(); 171 | } finally { 172 | error_reporting($currentReporting); 173 | } 174 | } 175 | 176 | /** 177 | * @group internal 178 | */ 179 | public function testRedefineThrowsAnExceptionForIncompatibleCallback(): void 180 | { 181 | $this->expectException(\ReflectionException::class); 182 | $expectedRegexp = '/"function \(\)" should be compatible with original "function \(\)\: \?string"/'; 183 | $this->expectExceptionMessageMatches($expectedRegexp); 184 | 185 | $this->refMethod->redefine(function () { 186 | echo 'Nope'; 187 | }); 188 | } 189 | 190 | /** 191 | * @group internal 192 | */ 193 | public function testRedefine(): void 194 | { 195 | $this->refMethod->redefine(function (): ?string { 196 | return 'Yes'; 197 | }); 198 | // Check that all main info were preserved 199 | $this->assertFalse($this->refMethod->isClosure()); 200 | $this->assertSame('reflectedMethod', $this->refMethod->getName()); 201 | 202 | $test = new TestClass(); 203 | $result = $test->reflectedMethod(); 204 | 205 | // Our method now returns Yes instead of class name 206 | $this->assertSame('Yes', $result); 207 | } 208 | 209 | public function testGetDeclaringClassReturnsCorrectInstance(): void 210 | { 211 | $class = $this->refMethod->getDeclaringClass(); 212 | $this->assertInstanceOf(ReflectionClass::class, $class); 213 | $this->assertSame(TestClass::class, $class->getName()); 214 | } 215 | 216 | /** 217 | * @group internal 218 | */ 219 | public function testSetDeclaringClass(): void 220 | { 221 | try { 222 | $this->refMethod->setDeclaringClass(self::class); 223 | $this->assertSame(self::class, $this->refMethod->getDeclaringClass()->getName()); 224 | } finally { 225 | $this->refMethod->setDeclaringClass(TestClass::class); 226 | } 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/Type/OpLine.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\Type; 14 | 15 | use FFI\CData; 16 | use ZEngine\Core; 17 | use ZEngine\Reflection\ReflectionValue; 18 | use ZEngine\System\ExecutionData; 19 | use ZEngine\System\OpCode; 20 | 21 | /** 22 | * Class OpLine represents one operation that should be performed by the engine 23 | * 24 | * struct _zend_op { 25 | * const void *handler; 26 | * znode_op op1; 27 | * znode_op op2; 28 | * znode_op result; 29 | * uint32_t extended_value; 30 | * uint32_t lineno; 31 | * zend_uchar opcode; 32 | * zend_uchar op1_type; 33 | * zend_uchar op2_type; 34 | * zend_uchar result_type; 35 | * }; 36 | */ 37 | class OpLine 38 | { 39 | /** 40 | * Unused operand 41 | */ 42 | public const IS_UNUSED = 0; 43 | 44 | /** 45 | * This opcode node type is used for literal values in PHP code. 46 | * 47 | * For example, the integer literal 1 or string literal 'Hello, World!' will both be of this type. 48 | */ 49 | public const IS_CONST = (1<<0); 50 | 51 | /** 52 | * This opcode node type is used for temporary variables. 53 | * 54 | * These are typically used to store an intermediate result of a larger operation (making them short-lived). 55 | * They can be an IS_TYPE_REFCOUNTED type (as of PHP 7), but not an IS_REFERENCE type 56 | * (since temporary values cannot be used as references). 57 | * 58 | * For example, the return value of $a++ will be of this type. 59 | */ 60 | public const IS_TMP_VAR = (1<<1); 61 | 62 | /** 63 | * This opcode node type is used for complex variables in PHP code. 64 | * 65 | * For example, the variable $obj->a is considered to be a complex variable, however the variable $a is not 66 | * (it is instead an IS_CV type). 67 | */ 68 | public const IS_VAR = (1<<2); 69 | 70 | /** 71 | * This opcode node type is used for simple variables in PHP code. 72 | * 73 | * For example, the variable $a is considered to be a simple variable, 74 | * however the variable $obj->a is not (it is instead an IS_VAR type). 75 | */ 76 | public const IS_CV = (1<<3); 77 | 78 | /** 79 | * Execution context (if present). 80 | * 81 | * It is used for resolving all temporary variables that are stored in 82 | */ 83 | private ?ExecutionData $context; 84 | 85 | /** 86 | * Stores the _zend_op * structure pointer 87 | */ 88 | private CData $opline; 89 | 90 | public function __construct(CData $opline, ExecutionData $context = null) 91 | { 92 | $this->opline = $opline; 93 | $this->context = $context; 94 | } 95 | 96 | /** 97 | * Returns a raw pointer to the opcode handler 98 | */ 99 | public function getHandler(): CData 100 | { 101 | return $this->opline->handler; 102 | } 103 | 104 | public function getOp1Type(): int 105 | { 106 | return $this->opline->op1_type; 107 | } 108 | 109 | public function getOp2Type(): int 110 | { 111 | return $this->opline->op2_type; 112 | } 113 | 114 | public function getOp1(): ?ReflectionValue 115 | { 116 | $value = $this->getValuePointer($this->opline->op1, $this->opline->op1_type); 117 | 118 | return $value; 119 | } 120 | 121 | public function getOp2(): ?ReflectionValue 122 | { 123 | $value = $this->getValuePointer($this->opline->op2, $this->opline->op2_type); 124 | 125 | return $value; 126 | } 127 | 128 | public function getResult(): ?ReflectionValue 129 | { 130 | $value = $this->getValuePointer($this->opline->result, $this->opline->result_type); 131 | 132 | return $value; 133 | } 134 | 135 | /** 136 | * Returns a defined code for this entry 137 | */ 138 | public function getCode(): int 139 | { 140 | return $this->opline->opcode; 141 | } 142 | 143 | /** 144 | * Directly replace an internal code with another one. 145 | * 146 | * DANGER! This can corrupt memory/engine state. 147 | * 148 | * @param int $newCode 149 | * @internal 150 | */ 151 | public function setCode(int $newCode): void 152 | { 153 | $this->opline->opcode->cdata = $newCode; 154 | } 155 | 156 | /** 157 | * Returns user-friendly name of the opCode 158 | */ 159 | public function getName(): string 160 | { 161 | $opCodeName = OpCode::name($this->opline->opcode); 162 | 163 | return $opCodeName; 164 | } 165 | 166 | /** 167 | * Returns the line in the code for which this opCode was generated 168 | */ 169 | public function getLine(): int 170 | { 171 | return $this->opline->lineno; 172 | } 173 | 174 | /** 175 | * Sets a new line for this entry 176 | * 177 | * @param int $newLine New line in the file 178 | * @internal 179 | */ 180 | public function setLine(int $newLine): void 181 | { 182 | $this->opline->lineno = $newLine; 183 | } 184 | 185 | /** 186 | * Returns the type name of operand 187 | * 188 | * @param int $opType Integer value of opType 189 | */ 190 | public static function typeName(int $opType): string 191 | { 192 | static $opTypeNames; 193 | if (!isset($opTypeNames)) { 194 | $opTypeNames = array_flip((new \ReflectionClass(self::class))->getConstants()); 195 | } 196 | 197 | return $opTypeNames[$opType] ?? 'UNKNOWN'; 198 | } 199 | 200 | /** 201 | * Returns a user-friendly representation of opCode line 202 | */ 203 | public function __debugInfo(): array 204 | { 205 | $humanCode = $this->getName(); 206 | $op1TypeName = self::typeName($this->opline->op1_type); 207 | $op2TypeName = self::typeName($this->opline->op2_type); 208 | $resTypeName = self::typeName($this->opline->result_type); 209 | 210 | return [ 211 | $humanCode => [ 212 | 'op1' => [$op1TypeName => $this->getOp1()], 213 | 'op2' => [$op2TypeName => $this->getOp2()], 214 | 'result' => [$resTypeName => $this->getResult()], 215 | 'line' => $this->getLine() 216 | ] 217 | ]; 218 | } 219 | 220 | /** 221 | * This utility function returns a pointer to value for given op_node and it's type 222 | * 223 | * @param CData $node Instance of op1/op2/result node 224 | * @param int $opType operation code type, eg IS_CONST, IS_CV... 225 | * 226 | * @return ReflectionValue|null Extracted value or null, if value could not be resolved (eg. not in runtime) 227 | * 228 | * @see zend_execute.c:zend_get_zval_ptr 229 | */ 230 | private function getValuePointer(CData $node, int $opType): ?ReflectionValue 231 | { 232 | $pointer = null; 233 | 234 | switch ($opType) { 235 | case self::IS_CONST: 236 | $pointer = self::getRuntimeConstant($this->opline, $node); 237 | break; 238 | case self::IS_TMP_VAR: 239 | case self::IS_VAR: 240 | case self::IS_CV: 241 | case self::IS_UNUSED: // For some opcodes IS_UNUSED still used, in most cases it points to an IS_UNDEF value 242 | // All these types requires context to be present, otherwise we can't resolve such nodes 243 | if (isset($this->context)) { 244 | $pointer = $this->context->getCallVariable($node->var); 245 | } 246 | break; 247 | default: 248 | throw new \InvalidArgumentException('Received invalid opcode type: ' . $opType); 249 | } 250 | $value = isset($pointer) ? ReflectionValue::fromValueEntry($pointer) : null; 251 | 252 | return $value; 253 | } 254 | 255 | /** 256 | * Returns value for a runtime-constant with IS_CONST type 257 | * 258 | * @see zend_compile.h:RT_CONSTANT macro definition 259 | * 260 | * @return CData zval* pointer 261 | */ 262 | private static function getRuntimeConstant(CData $opline, CData $node): CData 263 | { 264 | // ((zval*)(((char*)(opline)) + (int32_t)(node).constant)) 265 | $pointer = Core::cast('char *', $opline) + $node->constant; 266 | $value = Core::cast('zval *', $pointer); 267 | 268 | return $value; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/System/Compiler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\System; 14 | 15 | use FFI\CData; 16 | use ZEngine\AbstractSyntaxTree\NodeFactory; 17 | use ZEngine\AbstractSyntaxTree\NodeInterface; 18 | use ZEngine\Core; 19 | use ZEngine\Reflection\ReflectionValue; 20 | use ZEngine\Type\HashTable; 21 | use ZEngine\Type\StringEntry; 22 | 23 | class Compiler 24 | { 25 | /** 26 | * The following constants may be combined in CG(compiler_options) to change the default compiler behavior 27 | */ 28 | 29 | /* generate extended debug information */ 30 | public const COMPILE_EXTENDED_STMT = (1 << 0); 31 | public const COMPILE_EXTENDED_FCALL = (1 << 1); 32 | public const COMPILE_EXTENDED_INFO = (self::COMPILE_EXTENDED_STMT | self::COMPILE_EXTENDED_FCALL); 33 | 34 | /* call op_array handler of extendions */ 35 | public const COMPILE_HANDLE_OP_ARRAY = (1 << 2); 36 | 37 | /* generate INIT_FCALL_BY_NAME for internal functions instead of INIT_FCALL */ 38 | public const COMPILE_IGNORE_INTERNAL_FUNCTIONS = (1 << 3); 39 | 40 | /* don't perform early binding for classes inherited form internal ones; 41 | * in namespaces assume that internal class that doesn't exist at compile-time 42 | * may apper in run-time */ 43 | public const COMPILE_IGNORE_INTERNAL_CLASSES = (1 << 4); 44 | 45 | /* generate DECLARE_CLASS_DELAYED opcode to delay early binding */ 46 | public const COMPILE_DELAYED_BINDING = (1 << 5); 47 | 48 | /* disable constant substitution at compile-time */ 49 | public const COMPILE_NO_CONSTANT_SUBSTITUTION = (1 << 6); 50 | 51 | /* disable usage of builtin instruction for strlen() */ 52 | public const COMPILE_NO_BUILTIN_STRLEN = (1 << 7); 53 | 54 | /* disable substitution of persistent constants at compile-time */ 55 | public const COMPILE_NO_PERSISTENT_CONSTANT_SUBSTITUTION = (1 << 8); 56 | 57 | /* generate INIT_FCALL_BY_NAME for userland functions instead of INIT_FCALL */ 58 | public const COMPILE_IGNORE_USER_FUNCTIONS = (1 << 9); 59 | 60 | /* force ACC_USE_GUARDS for all classes */ 61 | public const COMPILE_GUARDS = (1 << 10); 62 | 63 | /* disable builtin special case function calls */ 64 | public const COMPILE_NO_BUILTINS = (1 << 11); 65 | 66 | /* result of compilation may be stored in file cache */ 67 | public const COMPILE_WITH_FILE_CACHE = (1 << 12); 68 | 69 | /* ignore functions and classes declared in other files */ 70 | public const COMPILE_IGNORE_OTHER_FILES = (1 << 13); 71 | 72 | /* this flag is set when compiler invoked by opcache_compile_file() */ 73 | public const COMPILE_WITHOUT_EXECUTION = (1 << 14); 74 | 75 | /* this flag is set when compiler invoked during preloading */ 76 | public const COMPILE_PRELOAD = (1 << 15); 77 | 78 | /* disable jumptable optimization for switch statements */ 79 | public const COMPILE_NO_JUMPTABLES = (1 << 16); 80 | 81 | /* this flag is set when compiler invoked during preloading in separate process */ 82 | public const COMPILE_PRELOAD_IN_CHILD = (1 << 17); 83 | 84 | /* The default value for CG(compiler_options) */ 85 | public const COMPILE_DEFAULT = self::COMPILE_HANDLE_OP_ARRAY; 86 | 87 | /* The default value for CG(compiler_options) during eval() */ 88 | public const COMPILE_DEFAULT_FOR_EVAL = 0; 89 | 90 | /** 91 | * Contains a hashtable with all registered classes 92 | * 93 | * @var HashTable|ReflectionValue[] 94 | */ 95 | public HashTable $classTable; 96 | 97 | /** 98 | * Contains a hashtable with all registered functions 99 | * 100 | * @var HashTable|ReflectionValue[] 101 | */ 102 | public HashTable $functionTable; 103 | 104 | /** 105 | * Contains a hashtable with all loaded files 106 | * 107 | * @var HashTable 108 | */ 109 | private HashTable $filenamesTable; 110 | 111 | /** 112 | * Holds an internal pointer to the compiler_globals structure 113 | */ 114 | private CData $pointer; 115 | 116 | public function __construct(CData $pointer) 117 | { 118 | $this->pointer = $pointer; 119 | $this->classTable = new HashTable($pointer->class_table); 120 | $this->functionTable = new HashTable($pointer->function_table); 121 | $this->filenamesTable = new HashTable($pointer->filenames_table); 122 | } 123 | 124 | /** 125 | * Checks if engine is compilation mode or not 126 | */ 127 | public function isInCompilation(): bool 128 | { 129 | return (bool) $this->pointer->in_compilation; 130 | } 131 | 132 | /** 133 | * Enables or disables compilation mode 134 | */ 135 | public function setCompilationMode(bool $enabled): void 136 | { 137 | $this->pointer->in_compilation = (int) $enabled; 138 | } 139 | 140 | /** 141 | * Returns the Abstract Syntax Tree for given source file 142 | */ 143 | public function getAST(): NodeInterface 144 | { 145 | if ($this->pointer->ast === null) { 146 | throw new \LogicException('Not in compilation process'); 147 | } 148 | 149 | return NodeFactory::fromCData($this->pointer->ast); 150 | } 151 | 152 | /** 153 | * Returns the file name which is compiled at the moment 154 | */ 155 | public function getFileName(): string 156 | { 157 | if ($this->pointer->compiled_filename === null) { 158 | throw new \LogicException('Not in compilation process'); 159 | } 160 | 161 | return StringEntry::fromCData($this->pointer->compiled_filename)->getStringValue(); 162 | } 163 | 164 | /** 165 | * Returns current compiler options 166 | */ 167 | public function getOptions(): int 168 | { 169 | return $this->pointer->compiler_options; 170 | } 171 | 172 | /** 173 | * Configures compiler options 174 | * 175 | * @param int $newOptions See COMPILER_xxx constants in this class 176 | */ 177 | public function setOptions(int $newOptions): void 178 | { 179 | $this->pointer->compiler_options = $newOptions; 180 | } 181 | 182 | /** 183 | * Performs parsing of PHP source code into the AST 184 | * 185 | * @param string $source Source code to parse 186 | * @param string $fileName Optional filename that will be used in the engine 187 | * 188 | * @return NodeInterface 189 | */ 190 | public function parseString(string $source, string $fileName = ''): NodeInterface 191 | { 192 | $sourceValue = new StringEntry($source); 193 | $sourceRaw = $sourceValue->getRawValue(); 194 | $rawSourceVal = ReflectionValue::newEntry(ReflectionValue::IS_STRING, $sourceRaw)->getRawValue(); 195 | 196 | $originalLexState = Core::new('zend_lex_state'); 197 | $originalCompilationMode = $this->isInCompilation(); 198 | $this->setCompilationMode(true); 199 | 200 | Core::call('zend_save_lexical_state', Core::addr($originalLexState)); 201 | 202 | $result = Core::call('zend_prepare_string_for_scanning', $rawSourceVal, $fileName); 203 | 204 | if ($result === Core::SUCCESS) { 205 | $this->pointer->ast = null; 206 | $this->pointer->ast_arena = $this->createArena(1024 * 32); 207 | $result = Core::call('zendparse'); 208 | if ($result !== Core::SUCCESS) { 209 | Core::call('zend_ast_destroy', $this->pointer->ast); 210 | $this->pointer->ast = null; 211 | Core::free($this->pointer->ast_arena); 212 | $this->pointer->ast_arena = null; 213 | } 214 | } 215 | 216 | // restore_lexical_state changes CG(ast) and CG(ast_arena) 217 | $ast = $this->pointer->ast; 218 | $node = NodeFactory::fromCData($ast); 219 | 220 | Core::call('zend_restore_lexical_state', Core::addr($originalLexState)); 221 | $this->setCompilationMode($originalCompilationMode); 222 | 223 | return $node; 224 | } 225 | 226 | /** 227 | * Creates an arena for misc needs 228 | * 229 | * @param int $size Size of arena to create 230 | * @see zend_arena.h:zend_arena_create 231 | */ 232 | private function createArena(int $size): CData 233 | { 234 | $rawBuffer = Core::new("char[$size]", false); 235 | $arena = Core::cast('zend_arena *', $rawBuffer); 236 | 237 | $arena->ptr = $rawBuffer + Core::getAlignedSize(Core::sizeof(Core::type('zend_arena'))); 238 | $arena->end = $rawBuffer + $size; 239 | $arena->prev = null; 240 | 241 | return $arena; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/AbstractSyntaxTree/Node.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * This source file is subject to the license that is bundled 8 | * with this source code in the file LICENSE. 9 | * 10 | */ 11 | declare(strict_types=1); 12 | 13 | namespace ZEngine\AbstractSyntaxTree; 14 | 15 | use FFI\CData; 16 | use ReflectionClass; 17 | use ZEngine\Core; 18 | use ZEngine\Reflection\ReflectionMethod; 19 | use function count; 20 | use function strpos; 21 | 22 | /** 23 | * General node class that can contain several children nodes 24 | * 25 | * typedef struct _zend_ast { 26 | * zend_ast_kind kind; 27 | * zend_ast_attr attr; 28 | * zend_uint lineno; 29 | * struct _zend_ast *child[1]; 30 | * } zend_ast; 31 | */ 32 | class Node implements NodeInterface 33 | { 34 | protected CData $node; 35 | 36 | /** 37 | * Creates an instance of Node 38 | * 39 | * @param int $kind Node kind 40 | * @param int $attributes Node attributes (like modifier, options, etc) 41 | * @param Node|null ...$nodes List of nested nodes (if required) 42 | */ 43 | public function __construct(int $kind, int $attributes, ?Node ...$nodes) 44 | { 45 | $nodeCount = count($nodes); 46 | $expectedCount = NodeKind::childrenCount($kind); 47 | if ($expectedCount !== $nodeCount || $nodeCount > 4) { 48 | $kindName = NodeKind::name($kind); 49 | $message = 'Given AST type ' . $kindName . ' expects exactly ' . $expectedCount . ' argument(s).'; 50 | throw new \InvalidArgumentException($message); 51 | } 52 | $funcName = "zend_ast_create_{$nodeCount}"; 53 | $arguments = []; 54 | foreach ($nodes as $index => $node) { 55 | if ($node === null) { 56 | $arguments[$index] = null; 57 | } else { 58 | $arguments[$index] = Core::cast('zend_ast *', $node->node); 59 | } 60 | } 61 | $node = Core::call($funcName, $kind, ...$arguments); 62 | $this->node = $node; 63 | $this->setAttributes($attributes); 64 | } 65 | 66 | /** 67 | * Node static constructor. 68 | */ 69 | public static function fromCData(CData $node): Node 70 | { 71 | /** @var self $instance */ 72 | $instance = (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); 73 | 74 | $instance->node = $node; 75 | 76 | return $instance; 77 | } 78 | 79 | /** 80 | * Returns the constant indicating the type of the AST node 81 | * 82 | * @see NodeKind class constants 83 | */ 84 | final public function getKind(): int 85 | { 86 | return $this->node->kind; 87 | } 88 | 89 | /** 90 | * Returns node's kind-specific flags 91 | */ 92 | final public function getAttributes(): int 93 | { 94 | return $this->node->attr; 95 | } 96 | 97 | /** 98 | * Changes node attributes 99 | */ 100 | final public function setAttributes(int $newAttributes): int 101 | { 102 | return $this->node->attr = $newAttributes; 103 | } 104 | 105 | /** 106 | * Returns the start line number of the node 107 | */ 108 | public function getLine(): int 109 | { 110 | return $this->node->lineno; 111 | } 112 | 113 | /** 114 | * Changes the node line 115 | */ 116 | public function setLine(int $newLine): void 117 | { 118 | $this->node->lineno = $newLine; 119 | } 120 | 121 | /** 122 | * Returns the number of children for this node 123 | */ 124 | public function getChildrenCount(): int 125 | { 126 | return NodeKind::childrenCount($this->node->kind); 127 | } 128 | 129 | /** 130 | * Returns children of this node 131 | * 132 | * @return NodeInterface[] 133 | */ 134 | final public function getChildren(): array 135 | { 136 | $totalChildren = $this->getChildrenCount(); 137 | if ($totalChildren === 0) { 138 | return []; 139 | } 140 | 141 | $children = []; 142 | $castChildren = Core::cast('zend_ast **', $this->node->child); 143 | for ($index = 0; $index < $totalChildren; $index++) { 144 | if ($castChildren[$index] !== null) { 145 | $children[$index] = NodeFactory::fromCData($castChildren[$index]); 146 | } else { 147 | $children[$index] = null; 148 | } 149 | } 150 | 151 | return $children; 152 | } 153 | 154 | /** 155 | * Return concrete child by index (can be empty) 156 | * 157 | * @param int $index Index of child node 158 | */ 159 | final public function getChild(int $index): ?NodeInterface 160 | { 161 | $totalChildren = $this->getChildrenCount(); 162 | if ($index >= $totalChildren) { 163 | throw new \OutOfBoundsException('Child index is out of range, there are ' . $totalChildren . ' children.'); 164 | } 165 | $castChildren = Core::cast('zend_ast **', $this->node->child); 166 | if ($castChildren[$index] === null) { 167 | return null; 168 | } 169 | 170 | return NodeFactory::fromCData($castChildren[$index]); 171 | } 172 | 173 | /** 174 | * Replace one child node with another one without checks 175 | * 176 | * @param int $index Child node index 177 | * @param NodeInterface $node New node to use 178 | */ 179 | public function replaceChild(int $index, NodeInterface $node): void 180 | { 181 | $totalChildren = $this->getChildrenCount(); 182 | if ($index >= $totalChildren) { 183 | throw new \OutOfBoundsException('Child index is out of range, there are ' . $totalChildren . ' children.'); 184 | } 185 | $castChildren = Core::cast('zend_ast **', $this->node->child); 186 | $castChildren[$index] = Core::cast('zend_ast *', $node->node); 187 | } 188 | 189 | /** 190 | * Removes a child node from the tree and returns the removed node. 191 | * 192 | * @param int $index Index of the node to remove 193 | */ 194 | public function removeChild(int $index): NodeInterface 195 | { 196 | $totalChildren = $this->getChildrenCount(); 197 | if ($index >= $totalChildren) { 198 | throw new \OutOfBoundsException('Child index is out of range, there are ' . $totalChildren . ' children.'); 199 | } 200 | $castChildren = Core::cast('zend_ast **', $this->node->child); 201 | $child = NodeFactory::fromCData($castChildren[$index]); 202 | 203 | $castChildren[$index] = null; 204 | 205 | return $child; 206 | } 207 | 208 | /** 209 | * This method is used to prevent segmentation faults when dumping CData 210 | */ 211 | final public function __debugInfo(): array 212 | { 213 | $result = []; 214 | $methods = (new ReflectionClass(static::class))->getMethods(ReflectionMethod::IS_PUBLIC); 215 | foreach ($methods as $method) { 216 | $methodName = $method->getName(); 217 | if ((strpos($methodName, 'get') === 0) && $method->getNumberOfRequiredParameters() === 0) { 218 | $name = lcfirst(substr($methodName, 3)); 219 | $result[$name] = $this->$methodName(); 220 | } 221 | } 222 | 223 | return $result; 224 | } 225 | 226 | /** 227 | * Dumps current node in friendly format 228 | * 229 | * @param int $indent Level of indentation 230 | */ 231 | final public function dump(int $indent = 0): string 232 | { 233 | $content = sprintf('%4d', $this->getLine()) . ': '; 234 | $content .= $this->dumpThis($indent) . "\n"; 235 | 236 | $childrenCount = $this->getChildrenCount(); 237 | if ($childrenCount > 0) { 238 | $children = $this->getChildren(); 239 | $content .= $this->dumpChildren($indent, ...$children); 240 | } 241 | 242 | return $content; 243 | } 244 | 245 | /** 246 | * Dumps current node itself (without children) 247 | */ 248 | protected function dumpThis(int $indent = 0): string 249 | { 250 | $line = str_repeat(' ', 2 * $indent); 251 | $line .= NodeKind::name($this->getKind()); 252 | 253 | $attributes = $this->getAttributes(); 254 | if ($attributes !== 0) { 255 | $line .= sprintf(" attrs(%04x)", $attributes); 256 | } 257 | 258 | return $line; 259 | } 260 | 261 | /** 262 | * Helper method to dump children nodes 263 | * 264 | * @param int $indent Current level of indentation 265 | * @param NodeInterface|null ...$nodes List of children nodes (can contain null values) 266 | */ 267 | private function dumpChildren(int $indent = 0, ?NodeInterface ...$nodes): string 268 | { 269 | $content = ''; 270 | foreach ($nodes as $index => $node) { 271 | if ($node === null) { 272 | continue; 273 | } 274 | $content .= $node->dump($indent + 1); 275 | } 276 | 277 | return $content; 278 | } 279 | } 280 | --------------------------------------------------------------------------------