microsoft / ClearScript

A library for adding scripting to .NET applications. Supports V8 (Windows, Linux, macOS) and JScript/VBScript (Windows).
https://microsoft.github.io/ClearScript/
MIT License
1.77k stars 148 forks source link

[FR]: Expose V8's CompileFunction (Along with the Rest of v8::ScriptCompiler) #583

Closed anonhostpi closed 4 months ago

anonhostpi commented 4 months ago

I've been working on porting Node.JS APIs over to PowerShell via ClearScript:

I've noticed that Node.JS uses V8's CompileFunction a lot for the underlying code behind internalBinding(). I also suspect that it is used for the vm.compileFunction().

For now, I think I can just get away with something similar to:

function Invoke-CompileFunction {
  param( 
    [Microsoft.ClearScript.V8.V8ScriptEngine] $engine,
    [string] $script_body,
    [string] $script_argnames
  )

  $wrapper = @"
with( null ) { # ScriptCompiler.CompileFunction's 3rd/4th argument populates the `with` keyword
  return function(
$( $script_argnames -join ", " )
  ){
$script_body
  }
}
"@

  return $engine.Compile( $wrapper )
}

Could you guys please expose the V8 compiler or the rest of its functions?

ClearScriptLib commented 4 months ago

Hi @anonhostpi,

We aren't clear on what this new method would provide over your existing solution. The other ScriptCompiler methods are already covered via ClearScript's Compile and CompileDocument, but V8's CompileFunction simply creates a function, and ClearScript already gives you a number of ways to do that.

Thanks!

anonhostpi commented 4 months ago

Just asking to level the API with node's, since they frequently use it in their source code.

I think the listed workaround should be fine. I just don't know if it suffers any kind of performance impact.

I know that for one, ClearScript's Compile and CompileDocument don't offer the setting of the with keyword.

Though, I'm sure that's not a big issue, because node doesn't use it either (and I'm pretty sure the with keyword is old and deprecated)

ClearScriptLib commented 4 months ago

Hi @anonhostpi,

Just asking to level the API with node's, since they frequently use it in their source code.

Wrapping V8's CompileFunction seems unnecessary. Script compilation can be useful, since a compiled script can be re-executed in another context without recompilation, and there's no other way to create such an object. Script functions, on the other hand, are always context-bound in V8, and there are already multiple ways to create them.

I think the listed workaround should be fine. I just don't know if it suffers any kind of performance impact.

It might be slightly worse than a dedicated path to V8's CompileFunction, but since the function is being created presumably for frequent re-execution, that one-time impact should be negligible. The other thing the workaround is missing is support for compilation cache data, but you could get that by using a compiled script to create the function.

I know that for one, ClearScript's Compile and CompileDocument don't offer the setting of the with keyword.

Support for "context extensions" (equivalent to with) applies only to function compilation, and you could add that to the workaround. Consider the following CompileFunction extension method:

public static class V8ScriptEngineExtensions {
    public static ScriptObject CompileFunction(this V8ScriptEngine engine, IEnumerable<string> argNames, string body, params object[] extensions) {
        var create = (ScriptObject)engine.Evaluate(@$"
            (function (...extensions) {{
                with (Object.assign({{}}, ...extensions))
                    return function ({string.Join(", ", argNames)}) {{ {body} }};
            }})
        ");
        return (ScriptObject)create.InvokeAsFunction(extensions);
    }
}

With that in place, you could do something like this:

dynamic greet = engine.CompileFunction(
    [ "audience" ],
    "Console.WriteLine(format, audience);",
    new { Console = typeof(Console).ToHostType(engine), format = "Hello, {0}!" }
);
greet("world");    // prints "Hello, world!"
greet("friends");  // prints "Hello, friends!"

That's just one C# example, but hopefully it gives you an idea.

Good luck!

anonhostpi commented 4 months ago

I was thinking along the same lines.

It does seem that Node does use CompileFunction specifically for context-specific compilation. They do also cache any of their compilations (context-specific or not).

It appears they use it as an optimized substitute for require

ClearScriptLib commented 4 months ago

It appears they use it as an optimized substitute for require

Oh, interesting! CommonJS modules depend on things like require and exports. The usual way to inject those is to wrap the module code within a function that takes them as arguments. That's what ClearScript does, as it works for both V8 and JScript, but CompileFunction could be a better solution for V8.

The above sample should work as long as V8 supports the deprecated with statement, whereas V8's CompileFunction would presumably continue working after that. Actually, we can modify the above so that it doesn't depend on with:

public static class V8ScriptEngineExtensions {
    public static ScriptObject CompileFunction(this V8ScriptEngine engine, IEnumerable<string> argNames, string body, params object[] extensions) {
        var createContext = (ScriptObject)engine.Evaluate(@"
            (function (...extensions) {
                return Object.assign({}, ...extensions);
            })
        ");
        var context = (IDictionary<string, object>)createContext.InvokeAsFunction(extensions);
        var createFunction = (ScriptObject)engine.Evaluate(@$"
            (function (context) {{
                const {{ {string.Join(", ", context.Keys)} }} = context;
                return function ({string.Join(", ", argNames)}) {{ {body} }};
            }})
        ");
        return (ScriptObject)createFunction.InvokeAsFunction(context);
    }
}

Anyway, please feel free to reopen this issue if you have additional thoughts or questions about this topic. Thank you!