Giter VIP home page Giter VIP logo

z-engine's Introduction

Z-Engine library

Build Status GitHub release Minimum PHP Version License

Have you ever dreamed about mocking a final class or redefining final method? Or maybe have an ability to work with existing classes in runtime? Z-Engine is a PHP7.4 library that provides an API to PHP. Forget about all existing limitations and use this library to transform your existing code in runtime by declaring new methods, adding new interfaces to the classes and even installing your own system hooks, like opcode compilation, object initalization and much more.

⚠️ DO NOT USE IT IN PRODUCTION UNTIL 1.0.0!

How it works?

As you know, PHP version 7.4 contains a new feature, called FFI. It allows the loading of shared libraries (.dll or .so), calling of C functions and accessing of C data structures in pure PHP, without having to have deep knowledge of the Zend extension API, and without having to learn a third "intermediate" language.

Z-Engine uses FFI to access internal structures of... PHP itself. This idea was so crazy to try, but it works! Z-Engine loads definition of native PHP structures, like zend_class_entry, zval, etc and manipulates them in runtime. Of course, it is dangerous, since FFI allows to work with structures on a very low level. Thus, you should expect segmentation faults, memory leaks and other bad things.

Pre-requisites and initialization

As this library depends on FFI, it requires PHP>=7.4 and FFI extension to be enabled. It should work in CLI mode without any troubles, whereas for web mode preload mode should be activated. Also, current version is limited to x64 non-thread-safe versions of PHP.

To install this library, simply add it via composer:

composer require lisachenko/z-engine

To activate a preload mode, please add Core::preload() call into your script, specified by opcache.preload. This call will be done during the server preload and will be used by library to bypass unnecessary C headers processing during each request.

Next step is to init library itself with short call to the Core::init():

use ZEngine\Core;

include __DIR__.'/vendor/autoload.php';

Core::init();

Now you can test it with following example:

<?php
declare(strict_types=1);

use ZEngine\Reflection\ReflectionClass;

include __DIR__.'/vendor/autoload.php';

final class FinalClass {}

$refClass = new ReflectionClass(FinalClass::class);
$refClass->setFinal(false);

eval('class TestClass extends FinalClass {}'); // Should be created

To have an idea, what you can do with this library, please see library tests as an example.

ReflectionClass

Library provides and extension for classic reflection API to manipulate internal structure of class via ReflectionClass:

  • setFinal(bool $isFinal = true): void Makes specified class final/non-final
  • setAbstract(bool $isAbstract = true): void Makes specified class abstract/non-abstract. Even if it contains non-implemented methods from interface or abstract class.
  • setStartLine(int $newStartLine): void Updates meta-information about the class start line
  • setEndLine(int $newEndLine): void Updates meta-information about the class end line
  • setFileName(string $newFileName): void Sets a new filename for this class
  • setParent(string $newParent) [WIP] Configures a new parent class for this one
  • removeParentClass(): void [WIP] Removes the parent class
  • removeTraits(string ...$traitNames): void [WIP] Removes existing traits from the class
  • addTraits(string ...$traitNames): void [WIP] Adds new traits to the class
  • removeMethods(string ...$methodNames): void Removes list of methods from the class
  • addMethod(string $methodName, \Closure $method): ReflectionMethod Adds a new method to the class
  • removeInterfaces(string ...$interfaceNames): void Removes a list of interface names from the class
  • addInterfaces(string ...$interfaceNames): void Adds a list of interfaces to the given class.

Beside that, all methods that return ReflectionMethod or ReflectionClass were decorated to return an extended object with low-level access to native structures.

ReflectionMethod

ReflectionMethods contains methods to work with a definition of existing method:

  • setFinal(bool $isFinal = true): void Makes specified method final/non-final
  • setAbstract(bool $isAbstract = true): void Makes specified method abstract/non-abstract.
  • setPublic(): void Makes specified method public
  • setProtected(): void Makes specified method protected
  • setPrivate(): void Makes specified method private
  • setStatic(bool $isStatic = true): void Declares method as static/non-static
  • setDeclaringClass(string $className): void Changes the declaring class name for this method
  • setDeprecated(bool $isDeprecated = true): void Declares this method as deprecated/non-deprecated
  • redefine(\Closure $newCode): void Redefines this method with a closure definition
  • getOpCodes(): iterable: [WIP] Returns the list of opcodes for this method

ObjectStore API

Every object in PHP has it's own unique identifier, which can be received via spl_object_id($object). Sometimes we are looking for the way to get an object by it's identifier. Unfortunately, PHP doesn't provide such an API, whereas internally there is an instance of zend_objects_store structure which is stored in the global executor_globals variable (aka EG).

This library provides an ObjectStore API via Core::$executor->objectStore which implements an ArrayAccess and Countable interface. This means that you can get any existing object by accessing this store with object handle:

use ZEngine\Core;

$instance = new stdClass();
$handle   = spl_object_id($instance);

$objectEntry = Core::$executor->objectStore[$handle];
var_dump($objectEntry);

Object Extensions API

With the help of z-engine library it is possible to overload standard operators for your classes without diving deep into the PHP engine implementation. For example, let's say you want to define native matrix operators and use it:

<?php

use ZEngine\ClassExtension\ObjectCastInterface;
use ZEngine\ClassExtension\ObjectCompareValuesInterface;
use ZEngine\ClassExtension\ObjectCreateInterface;
use ZEngine\ClassExtension\ObjectCreateTrait;
use ZEngine\ClassExtension\ObjectDoOperationInterface;

class Matrix implements
    ObjectCreateInterface,
    ObjectCompareValuesInterface,
    ObjectDoOperationInterface,
    ObjectCastInterface
{
    use ObjectCreateTrait;

    // ...
}
$a = new Matrix([10, 20, 30]);
$b = new Matrix([1, 2, 3]);
$c = $a + $b; // Matrix([11, 22, 33])
$c *= 2;      // Matrix([22, 44, 66])

There are two ways of activating custom handlers. First way is to implement several system interfaces like ObjectCastInterface, ObjectCompareValuesInterface, ObjectCreateInterface and ObjectDoOperationInterface. After that you should create an instance of ReflectionClass provided by this package and call installExtensionHandlers method to install extensions:

use ZEngine\Reflection\ReflectionClass as ReflectionClassEx;

// ... initialization logic

$refClass = new ReflectionClassEx(Matrix::class);
$refClass->installExtensionHandlers();

if you don't have an access to the code (eg. vendor), then you can still have an ability to define custom handlers. You need to define callbacks as closures explicitly and assign them via set***Handler() methods in the ReflectionClass.

use ZEngine\ClassExtension\ObjectCreateTrait;
use ZEngine\Reflection\ReflectionClass as ReflectionClassEx;

$refClass = new ReflectionClassEx(Matrix::class);
$handler  = Closure::fromCallable([ObjectCreateTrait::class, '__init']);
$refClass->setCreateObjectHandler($handler);
$refClass->setCompareValuesHandler(function ($left, $right) {
    if (is_object($left)) {
        $left = spl_object_id($left);
    }
    if (is_object($right)) {
        $right = spl_object_id($right);
    }

    // Just for example, object with bigger object_id is considered bigger that object with smaller object_id
    return $left <=> $right;
});

Library provides following interfaces:

First one is ObjectCastInterface which provides a hook for handling casting a class instance to scalars. Typical examples are following: 1) explicit $value = (int) $objectInstance or implicit: $value = 10 + $objectInstance; in the case when do_operation handler is not installed. Please note, that this handler doesn't handle casting to array type as it is implemented in a different way.

<?php
use ZEngine\ClassExtension\Hook\CastObjectHook;

/**
 * Interface ObjectCastInterface allows to cast given object to scalar values, like integer, floats, etc
 */
interface ObjectCastInterface
{
    /**
     * Performs casting of given object to another value
     *
     * @param CastObjectHook $hook Instance of current hook
     *
     * @return mixed Casted value
     */
    public static function __cast(CastObjectHook $hook);
}

To get the type of casting, you should check $hook->getCastType() method which will return the integer value of type. Possible values are declared as public constants in the ReflectionValue class. For example ReflectionValue::IS_LONG.

Next ObjectCompareValuesInterface interface is used to control the comparison logic. For example, you can compare two objects or even compare object with scalar values: if ($object > 10 || $object < $anotherObject)

<?php
use ZEngine\ClassExtension\Hook\CompareValuesHook;

/**
 * Interface ObjectCompareValuesInterface allows to perform comparison of objects
 */
interface ObjectCompareValuesInterface
{
    /**
     * Performs comparison of given object with another value
     *
     * @param CompareValuesHook $hook Instance of current hook
     *
     * @return int Result of comparison: 1 is greater, -1 is less, 0 is equal
     */
    public static function __compare(CompareValuesHook $hook): int;
}

Handler should check arguments which can be received by calling $hook->getFirst() and $hook->getSecond() methods (one of them should return an instance of your class) and return integer result -1..1. Where 1 is greater, -1 is less and 0 is equal.

The interface ObjectDoOperationInterface is the most powerful one because it gives you control over math operators applied to your object (such as ADD, SUB, MUL, DIV, POW, etc).

<?php
use ZEngine\ClassExtension\Hook\DoOperationHook;

/**
 * Interface ObjectDoOperationInterface allows to perform math operations (aka operator overloading) on object
 */
interface ObjectDoOperationInterface
{
    /**
     * Performs an operation on given object
     *
     * @param DoOperationHook $hook Instance of current hook
     *
     * @return mixed Result of operation value
     */
    public static function __doOperation(DoOperationHook $hook);
}

This handler receives an opcode (see OpCode::* constants) via $hook->getOpcode() and two arguments (one of them is an instance of class) via $hook->getFirst() and $hook->getSecond() and returns a value for that operation. In this handler you can return a new instance of your object to have a chain of immutable instances of objects.

Important reminder: you MUST install the create_object handler first in order to install hooks in runtime. Also you can not install the create_object handler for the object if it is internal one.

There is one extra method called setInterfaceGetsImplementedHandler which is useful for installing special handler for interfaces. The interface_gets_implemented callback uses the same memory slot as create_object handler for object, and will be called each time when any class will implement this interface. This gives interesting options for automatic class extensions registration, for example, if a class implements the ObjectCreateInterface then automatically call ReflectionClass->installExtensionHandlers() for it in callback.

Abstract Syntax Tree API

As you know, PHP7 uses an abstract syntax tree for working with abstract model of source code to simplify future development of language syntax. Unfortunately, this information is not provided back to the userland level. There are several PHP extensions like nikic/php-ast and sgolemon/astkit that provide low-level bindings to the underlying AST structures. Z-Engine provides access to the AST via Compiler::parseString(string $source, string $fileName = '') method. This method will return a top-level node of tree that implements NodeInterface. PHP has four types of AST nodes, they are: declaration node (classes, methods, etc), list node (can contain any number of children nodes), simple node (contains up to 4 children nodes, depending of type) and special value node class that can store any value in it (typically string or numeric).

Here are an example of parsing simple PHP code:

use ZEngine\Core;

$ast = Core::$compiler->parseString('echo "Hello, world!", PHP_EOL;', 'hi.php');
echo $ast->dump();

Output will be like that:

   1: AST_STMT_LIST
   1:   AST_STMT_LIST
   1:     AST_ECHO
   1:       AST_ZVAL string('Hello, world!')
   1:     AST_ECHO
   1:       AST_CONST
   1:         AST_ZVAL attrs(0001) string('PHP_EOL')

Node provides simple API to mutate children nodes via call to the Node->replaceChild(int $index, ?Node $node). You can create your own nodes in runtime or use a result from Compiler::parseString(string $source, string $fileName = '') as replacement for your code.

Modifying the Abstract Syntax Tree

When PHP 7 compiles PHP code it converts it into an abstract syntax tree (AST) before finally generating Opcodes that are persisted in Opcache. The zend_ast_process hook is called for every compiled script and allows you to modify the AST after it is parsed and created.

To install the zend_ast_process hook, make a static call to the Core::setASTProcessHandler(Closure $callback) method that accepts a callback which will be called during AST processing and will receive a AstProcessHook $hook as an argument. You can access top-level node item via $hook->getAST(): NodeInterface method.

use ZEngine\Core;
use ZEngine\System\Hook\AstProcessHook;

Core::setASTProcessHandler(function (AstProcessHook $hook) {
    $ast = $hook->getAST();
    echo "Parsed AST:", PHP_EOL, $ast->dump();
    // Let's modify Yes to No )
    echo $ast->getChild(0)->getChild(0)->getChild(0)->getValue()->setNativeValue('No');
});

eval('echo "Yes";');

// Parsed AST:
//    1: AST_STMT_LIST
//    1:   AST_STMT_LIST
//    1:     AST_ECHO
//    1:       AST_ZVAL string('Yes')
// No

You can see that result of evaluation is changed from "Yes" to "No" because we have adjusted given AST in our callback. But be aware, that this is one of the most complicated hooks to use, because it requires perfect understanding of the AST possibilities. Creating an invalid AST here can cause weird behavior or crashes.

Creating PHP extensions in runtime

The most interesting part of Z-Engine library is creating your own PHP extensions in PHP language itself. You do not have to spend a lot of time learning the C language; instead, you can use the ready-made API to create your own extension module from PHP itself!

Of course, not everything is possible to implement as an extension in PHP, for example, changing the parser syntax or changing the logic of opcache - for this you will have to delve into the code of the engine itself.

Let's make an example a module with global variables, an analog of apcu, so that these variables are not cleared after the request is completed. It is believed that PHP has the concept of share nothing and therefore can’t survive the boundary of the request, since at the time of completion of the request PHP will automatically free all allocated memory for objects. However, PHP itself can work with global variables, and they are stored inside loaded modules by the pointer zend_module_entry.globals_ptr.

Therefore, if we can register the module in PHP and allocate global memory for it, PHP will not clear it, and our module will be able to survive the boundary of the request.

Technically, every module is represented by following structure:

struct _zend_module_entry {
    unsigned short size;
    unsigned int zend_api;
    unsigned char zend_debug;
    unsigned char zts;
    const struct _zend_ini_entry *ini_entry;
    const struct _zend_module_dep *deps;
    const char *name;
    const struct _zend_function_entry *functions;
    int (*module_startup_func)(int type, int module_number);
    int (*module_shutdown_func)(int type, int module_number);
    int (*request_startup_func)(int type, int module_number);
    int (*request_shutdown_func)(int type, int module_number);
    void (*info_func)(zend_module_entry *zend_module);
    const char *version;
    size_t globals_size;
#ifdef ZTS
    ts_rsrc_id* globals_id_ptr;
#endif
#ifndef ZTS
    void* globals_ptr;
#endif
    void (*globals_ctor)(void *global);
    void (*globals_dtor)(void *global);
    int (*post_deactivate_func)(void);
    int module_started;
    unsigned char type;
    void *handle;
    int module_number;
    const char *build_id;
};

You can see that we can define several callbacks and there are several fields with meta-information about zts, debug, API version, etc that are used by PHP to check if this module can be loaded for current environment.

From PHP side, you should extend your module class from the AbstractModule class that contains general logic of module registration and startup and implement all required method from the ModuleInterface.

Let's have a look at our simple module:

use ZEngine\EngineExtension\AbstractModule;

class SimpleCountersModule extends AbstractModule
{
    /**
     * Returns the target thread-safe mode for this module
     *
     * Use ZEND_THREAD_SAFE as default if your module does not depend on thread-safe mode.
     */
    public static function targetThreadSafe(): bool
    {
        return ZEND_THREAD_SAFE;
    }

    /**
     * Returns the target debug mode for this module
     *
     * Use ZEND_DEBUG_BUILD as default if your module does not depend on debug mode.
     */
    public static function targetDebug(): bool
    {
        return ZEND_DEBUG_BUILD;
    }

    /**
     * Returns the target API version for this module
     *
     * @see zend_modules.h:ZEND_MODULE_API_NO
     */
    public static function targetApiVersion(): int
    {
        return 20190902;
    }

    /**
     * Returns true if this module should be persistent or false if temporary
     */
    public static function targetPersistent(): bool
    {
        return true;
    }

    /**
     * Returns globals type (if present) or null if module doesn't use global memory
     */
    public static function globalType(): ?string
    {
        return 'unsigned int[10]';
    }
}

Our SimpleCountersModule declares that it will use array of 10 unsigned ints. It also provides some information about required environment (debug/zts/API version). Important option is to mark our module persistent by returning true from targetPersistent() method. And now we are ready to register it and use it:

$module = new SimpleCountersModule();
if (!$module->isModuleRegistered()) {
    $module->register();
    $module->startup();
}

$data = $module->getGlobals();
var_dump($data);

Note, that on subsequent requests module will be registered, this is why you should not call register twice. What is really cool is that any changes in module globals are true globals! They will be preserved between requests. Try to update each item to see that values in our array are increasing between requests:

$index        = mt_rand(0, 9); // If you have several workers, you should use worker pid to avoid race conditions
$data[$index] = $data[$index] + 1; // We are increasing global counter by one

/* Example of var_dump after several requests...
object(FFI\CData:uint32_t[10])#35 (10) {
  [0]=>
  int(1)
  [1]=>
  int(1)
  [2]=>
  int(1)
  [3]=>
  int(3)
  [4]=>
  int(1)
  [5]=>
  int(1)
  [6]=>
  int(1)
  [7]=>
  int(2)
  [8]=>
  int(2)
  [9]=>
  int(2)
}*/

Of course, module can declare any complex structure for globals and use it as required. If module requires some initialization, then you can implement the ControlModuleGlobalsInterface in your module and this callback will be called during module startup procedure. This may be useful for registration of additional hooks, class extensions, etc or for global variable initialization (filling it with predefined values, restoring state from DB/filesystem/etc)

Code of Conduct

This project adheres to the Contributor Covenant code of conduct. By participating, you are expected to uphold this code. Please report any unacceptable behavior.

License

This library is licensed under the MIT license.

Creating and maintaining this library is endless hard work for me. That's why there is one simple requirement for you: please give something back to the world. Whether that's code or financial support for this project is entirely up to you.

z-engine's People

Contributors

dependabot-preview[bot] avatar lisachenko avatar mateuszjozwik1 avatar szepeviktor avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

z-engine's Issues

[FAQ] Licensing

This issue clarifies the usage of this library with other projects and describes terms of usage.

  • This library, provided under RPL license can be used for free for any open-source projects without any limitations while source code of these projects or libraries is also open-sourced.
  • For open-source libraries that are distributed under more permissive license types like MIT and GPL you could obtain a free persistent premium license for your open-source project either by contributing to this project itself (I accept PR with tests, documentation and new features, bug fixes are not counted) or by applying the treeware license to your project. Your project should also mention explicitly that it is powered by the Z-Engine in your readme file.
  • For commercial use, please contact me via email lisachenko.it at gmail.com. For small companies (up to 10 developers) it will cost about $100/year only which can be compared to the price of PHP developer per one hour. This should be acceptable price of speeding up your development or using extra powerful extensions built on top of Z-Engine library.

I'm going to spend most of received money on charity (mostly related to climate programs) and supporting other open-source projects.

You could quickly find projects that are built on top of the Z-Engine by clicking the изображение button at the top.

[Feature] Provide a read-only API to the global object store

All objects in PHP are also stored in the EG(objects_store) which is plain array of references to the object entries.
This library can provide API for this store to query an object by its identifier/handle, available via spl_object_id(). ObjectStore should implement Countable and ArrayAccess interfaces to access objects in the store. It would be nice to wrap each returned zend_object entry into the ObjectEntry instance to have full control over objects.

[Feature] Concept of userland PHP extensions provided as PHP libraries

I have learned a lot about PHP native C extensions and it looks possible to implement a mechanism that will provide an API for development of extensions for userland developers. Every extension is registered in the global registry, called module_registry. It is a simple hash table that contains key-value pairs of pointers to the zend_module_entry entry. And there is main API method zend_register_module_ex which can be used to register new module. So, it should be doable at least...

What is interesting is that combining FFI with userland modules can give a way to survive request boundaries and avoid troubles with memory leaks in FFI itself, because extension can be persistent and PHP will keep it between requests.

In practice this means that it will be possible to carry data or even objects between requests.

Dead code detection?

Is it possible to use this lib for dead-code detection? If it is, then it will be good to state it in README.
Or provide some example.

[Bug] Fix opcache compilation warning

During preloading of all files from the src folder, there are several loaded core files, thus opcache_compile_file triggers warning about existing classes in memory.

Need to verify if script is cached via opcache_is_script_cached($fileName) function.

Fix memory leak and heap corruption during trait manipulation

This issue pretty same as #9, but relates to the traits. Receipt as previous: for internal classes we should use persistent memory, for user-defined non-persistent memory should be used.

Also, for user-defined classes previously allocated memory should be freed to prevent leaks.

Is it possible to handle '===' opcode?

How to intercept is identical (===) check ? e.g.

$o1 == $o2;

will trigger __compare() handler, while

$o1 === $o2;

doesn't trigger neither __compare() nor __doOperation() with \ZEngine\System\OpCode::IS_IDENTICAL;

[Bug] Method ReflectionClass->addMethod is not compatible with opcache

With enabled opcache, function redefinition doesn't work:

1) ZEngine\Reflection\ReflectionFunctionTest::testRedefine
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
-'Yes'
+'Test'

~/z-engine/tests/Reflection/ReflectionFunctionTest.php:84

Also, there is an assertion error:

~/src/php-src/Zend/zend_opcode.c:134: zend_function_dtor: Assertion `function->type == 1' failed.

After that PHP is crashing during request shutdown:

Program received signal SIGABRT, Aborted.
0x00007ffff5acc428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
54      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) bt
#0  0x00007ffff5acc428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007ffff5ace02a in __GI_abort () at abort.c:89
#2  0x00007ffff5ac4bd7 in __assert_fail_base (fmt=<optimized out>, assertion=assertion@entry=0x123b8b7 "function->type == 1",
    file=file@entry=0x123b860 "~/src/php-src/Zend/zend_opcode.c", line=line@entry=134, function=function@entry=0x123c820 <__PRETTY_FUNCTION__.11912> "zend_function_dtor")
    at assert.c:92
#3  0x00007ffff5ac4c82 in __GI___assert_fail (assertion=0x123b8b7 "function->type == 1", file=0x123b860 "~/src/php-src/Zend/zend_opcode.c", line=134,
    function=0x123c820 <__PRETTY_FUNCTION__.11912> "zend_function_dtor") at assert.c:101
#4  0x00000000008e91aa in zend_function_dtor (zv=0x7ffff2a63f60) at ~/src/php-src/Zend/zend_opcode.c:134
#5  0x0000000000914c79 in zend_hash_destroy (ht=0x7ffff2bb3800) at ~/src/php-src/Zend/zend_hash.c:1541
#6  0x00000000008e9ebe in destroy_zend_class (zv=0x1811a60) at ~/src/php-src/Zend/zend_opcode.c:313
#7  0x00000000008e44bd in shutdown_executor () at ~/src/php-src/Zend/zend_execute_API.c:393
#8  0x00000000008fc8b6 in zend_deactivate () at ~/src/php-src/Zend/zend.c:1186
#9  0x0000000000867d00 in php_request_shutdown (dummy=0x0) at ~/src/php-src/main/main.c:1923
#10 0x00000000009d8d57 in do_cli (argc=2, argv=0x163f0e0) at ~/src/php-src/sapi/cli/php_cli.c:1129
#11 0x00000000009d95de in main (argc=2, argv=0x163f0e0) at ~/src/php-src/sapi/cli/php_cli.c:1356

Question about typehints

Sorry that I don't fully understand the abilities of FFI so I have to ask; is it possible to drop typehint check in generated opcode?

Reason 1: performance. I have few apps that deal with millions of callbacks like this (totally simplified example, real one has 8 methods running for each $result):

foreach($someGenerator as $result) {
    doSomething($result);
}

function doSomething(Product $product)  // <-  this is the issue

I use psalm and it is impossible to make a mistake and not see it. Even if I make one, during development runtime checks would still work; my question is about production.

The idea is not to use preprocess.io or similar or fallback to @param annotations; typehints must stay so reflection would still work (autowiring). It is only to remove opcode that verifies params/return types.


Reason 2: generics

If engine could transparently convert code like this

// from 
function doSomething(Collection<string, Product> $products)

// to
function doSomething(Collection $products)

with PHPStorm and psalm/phpstan support we could possibly have reified generics. Transparently means delete everything between < and > before giving it to compiler.

Problem

This would effectively remove TypeError exception but I don't expect many libraries rely on it for logic (if any). PHPUnit and similar are not business logic.


Another solution would be file-based config. So not all files would ignore typehint check but only if there is a declare at top.

Add Windows platform support

Currently this library uses some exported functions for hashes and opcode handler manipulation. Unfortunately, under Windows platform, PHP uses __vectorcall convention which is not supported by libffi, thus symbol can't be resolved.

This issue can be solved in two ways: by adding support for __vectorcall convention in libffi or by writing hash functions from scratch, which is not easy task right now.

[Bug] Incorrect scope check for property hooks

After implementation of #31, new issue is appeared: PHP FFI trampoline which is called during hook invocation is changing original scope to another one (FFI) and this is unexpcted for PHP engine. After that constructor in a class with property hooks can not access private or protected members in the same class because of incorrect scope.

To fix this issue, EG(fake_scope) could be used.

[Feature] Provide integration with zend_ast_process hook

PHP provides a zend_ast_process hook. It is called for every compiled script and allows to modify the AST after it is parsed and created. This hook can be used for runtime source-code transformation, optimization, etc as it will be possible to completely rewrite AST with another one.

For example of AST transformation, please see https://github.com/GoogleCloudPlatform/stackdriver-debugger-php-extension/blob/master/docs/design.md#modifying-source-at-compilation-time

This feature also should be useful for AOP, Design-by-Contract frameworks, tracing tools and much more.

[Feature] Provide userland AST parsing API

PHP7 uses an abstract syntax tree during compilation of source code which can be used for future transformation and code analysis. Need to add implementation of general node types, as well, simple parse method that will invoke internal methods zendparse() to access AST.

Later, integration with zend_ast_process can be added to provide a general userland hook implementation as was suggested in the RFC: Parser Extension API.

Add automatic self-registration via Composer

Autoload can be used for automatic Core initialization, thus simplify usage of this library. Unfortunately, Composer doesn't support automatic preload right now, so only CLI mode can be used for auto-registration right now.

[Feature] Provide an access to opcode's operands

This feature is required to provide user-defined opcode handlers, otherwise it won't be possible to resolve values in current context.

Opcode contains two nodes and one return value, by default, most of them are stored in the current stack frame (except the IS_CONST values that are inlined into opcode memory itself).

Did you get `FFI::load()` to work in preload?

FFI::load($tempFile);

I could not get FFI::load() to work in preload. Have you tested this and verified that it works? What does your php.ini opcache and ffi config look like for a working preload setup?

The problem in my tests is that opcache.preload requires opcache.preload_user to be set, which in turn disables FFI::load() if set. Which in turn means I have to manually set header.h in the ffi.preload INI config.

The aim would be to have FFI::load() inside preload, and FFI::scope() outside preload.

PHP 8 support?

Hey, thanks for the lib.

Do you have any plans to support PHP 8?

Investigate an assertion error in zend_string_destroy

Test suite fails in PHP 7.4.0RC3 debug build with message:

php-src/Zend/zend_variables.c:64: zend_string_destroy: Assertion `!(zval_gc_flags((str)->gc.u.type_info) & (1<<6))' failed.

Need to fix this assertion error.

Enable FFI+Opcache preload optimization

Loading/parsing definition of C headers in runtime is not good, also, ffi.enable is equal to preload by default, thus, to make this library active by default, preloading of C headers should be added.

Ideally, this requires some infrastructure support to activate FFI preloading.

  • Wait for integration of preloading into composer composer/composer#7777
  • Add cache-warming script that loads definition of headers during preload stage and compiles all library classes to be available by default to reduce class-loading time and compilation.
  • Check long-running scripts (FPM, Standalone server) to ensure that there are no memory leaks and segfaults
  • Wait for PHP7.4.2 to have a bug fix for: https://bugs.php.net/bug.php?id=78918
  • For Windows platform, preloading is not available anymore since 7.4.2 php/php-src#4999

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.