phpv8 / php-v8

PHP extension for V8 JavaScript engine
https://php-v8.readthedocs.io
MIT License
217 stars 14 forks source link

Question: Module support #23

Closed aight8 closed 7 years ago

aight8 commented 7 years ago

Hei, so much respect for this library it's awesome :) I got it to work and wrote a wrapper arround it after lot of experiments and It was educational, will publish it soon. I have a question regarding modules. I want to use modules on the common js way like in node.js. I saw some hints in the php stub about modules, but I don't know how to get it to work. Is this already working? If yes, can you give me any hints how to implement some easy usecases? Thank you very much!

pinepain commented 7 years ago

Hi Sebastian, thanks for writing!

It would be nice to see your wrapper as I also have a one, though, it's heavily in progress at this time and I don't publish it to github yet.

As to modules, there are two types of it from API perspective: native and source-based module. With a native one it's pretty simple: you return some value for require() call.

With source-based you may follow simple path: populate context's global object with necessary exports, require, module variable and then return modules.exports result or you can do like node.js does - wrap module source code and evaluate it with passing necessary values for exports, require, module and so on.

As for caching, it's pretty trivial, just maintain an array of normalized module name to what it exports and on consecutive calls return that value. This is how node.js does afair.

Here is very basic require() implementation I use in one of tests - tests/V8FunctionTemplate_require_implementation.phpt

pinepain commented 7 years ago

Closing this issue for now as it not much to do at this time with it. If you feels there is something we missed here - please, drop a comment or feels free to open a new issue.

I'm also working on a library for php-v8 which also provides node-like modules functionality, however, no ETA at this time.

aight8 commented 7 years ago

Sorry for the late response. Thanks for the explanation, I see it's little bit more work than expected. I saw the the parallel v8 extension for php that they had a hook function which get the requested module as string passed and you have to return the code/path (don't know anymore). But I thought v8 do a little bit more since it's "module" flag.

I will post the wrapper ASAP here.

pinepain commented 7 years ago

The simplest implementation for require() is available in tests/V8FunctionTemplate_require_implementation.phpt:26:41. While php-v8 is more low-level than v8js, there is a bit more to do pass data back to v8 runtime, but basically, if you replace tests/V8FunctionTemplate_require_implementation.phpt:33:37 with calling your external function, you should get very similar behavior to v8js.

I guess something like this should do the job:

Note, this is meta-code which may need to be polished in order to work

use V8\Value;
use V8\Context;
use V8\NumberValue;
use V8\StringValue;
use V8\ObjectValue;
use V8\Script;
use V8\ScriptOrigin;
use V8\FunctionTemplate;
use V8\FunctionCallbackInfo;

interface ModuleLoaderInterface {
    public function load(string $name, Context $context) : Value;
}

class ModuleLoader implements ModuleLoaderInterface {
    public function load(string $name, Context $context): Value
    {
        $isolate = $context->GetIsolate();

        $module_is_native = 'TODO'; // figure it out

        // here you can actually evaluate your script
        if ($module_is_native) {
            // in real life you will normally build necessary v8 value and return it
            return new NumberValue($isolate, 42);
        }

        $path = $name;
        $source = 'TODO'; // you will actually get it somehow from your db, filesystem or so

        $wrapped_source =
            "(function (exports, require, module, __filename, __dirname) {\n" .
            $source . // you will actually get it somehow from your db, filesystem or so
            "\n});";

        $script = new Script($context, new StringValue($isolate, $wrapped_source), new ScriptOrigin($path, -1));

        $func = $script->Run($context);

        $exports = new ObjectValue($context);
        $module_obj = new ObjectValue($context);  // you actually have to build it properly - https://nodejs.org/api/modules.html#modules_the_module_object
        $require = $context->GlobalObject()->Get($context, new StringValue($isolate, 'require');
        $filename = new StringValue($isolate, $path);
        $dirname = new StringValue($isolate, dirname($path));

        // exports, require, module, __filename, __dirname
        $arguments = [$exports, $require, $module_obj, $filename, $dirname];

        return $func->Call($context, $func, $arguments);
    }
}

$loader = new ModuleLoader();

$require_func_tpl_cache = new FunctionTemplate($isolate, function (FunctionCallbackInfo $info) use (&$loaded_cache, &$code, $loader) {
    $context = $info->GetContext();
    $module = $info->Arguments()[0]->ToString($context)->Value();
    if (!isset($loaded_cache[$module])) {
        $loaded_cache[$module] = $loader->load($module, $context);
    }
    $info->GetReturnValue()->Set($loaded_cache[$module]);
});

or even

class CacheableModuleLoader implements ModuleLoaderInterface
{
    /**
     * @var Value[]
     */
    private $cache = [];
    /**
     * @var ModuleLoaderInterface
     */
    private $loader;

    public function __construct(ModuleLoaderInterface $loader)
    {
        $this->loader = $loader;
    }

    public function load(string $name, Context $context): Value
    {
        if (!isset($this->cache[$name])) {
            $this->cache[$name] = $this->loader->load($name, $context);
        }

        return $this->cache[$name];
    }
}

$loader = new CacheableModuleLoader(new ModuleLoader());

$require_func_tpl_cache = new FunctionTemplate($isolate, function (FunctionCallbackInfo $info) use (&$loaded_cache, &$code, $loader) {
    $context = $info->GetContext();
    $module = $info->Arguments()[0]->ToString($context)->Value();

    $info->GetReturnValue()->Set($loader->load($module, $context));
});
pinepain commented 7 years ago

Thank you for bringing up this question. I plan to release higher level wrapper for php-v8 which should include modules support, so I'm closing this issue for now.