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.74k stars 148 forks source link

How can I check if a clearscript v8scriptitem is a function or an object #473

Closed gjvdkamp closed 1 year ago

gjvdkamp commented 1 year ago

Hi,

The way we use V8 has user type code pretty freely, so I don't know what variables they're going to declare and if they'll write functions etc. I want to dump what they're making into a window, and cerate extra functionality for that, for that I need to be able to tell the types of what they have created. Right now I convert things to NewtonSoft Json but then then functions end up as empty objects {}..

I asked ChatGPT and got all excited 😭

Hoping you still know better :)

image

ClearScriptLib commented 1 year ago

Hi @gjvdkamp,

I asked ChatGPT and got all excited 😭

Wow, ChatGPT is very impressive... yet very wrong in this case 🤣

Often the best way to deal with a script object is to use the script engine. Consider the following:

dynamic isFunctionImpl = engine.Evaluate("obj => obj instanceof Function");
bool IsFunction(object value) => value is ScriptObject obj && isFunctionImpl(obj);

Now you have an easy way to perform the test:

Console.WriteLine(IsFunction(engine.Evaluate("Math")));       // output: False
Console.WriteLine(IsFunction(engine.Evaluate("Math.sqrt")));  // output: True

Good luck!

gjvdkamp commented 1 year ago

Ah very clever to only go to the engine to verify for ScriptObjects that should speed it up considerably. Thanks so much!

ClearScriptLib commented 1 year ago

Ah very clever to only go to the engine to verify for ScriptObjects that should speed it up considerably.

Actually, you can perform several checks without invoking the engine at all. For example, some V8 script objects, when brought over to the host, implement managed interfaces such as IArrayBuffer, IDataView, and ITypedArray<T>. Normal V8 script arrays similarly implement IList. And there are internal ways to quickly identify promises and other things. Not so for functions, unfortunately, and there's currently no consistent, comprehensive public API for such tests 😔

gjvdkamp commented 1 year ago

Hi, in the end I went for this approach where I first check if it's an array, then distinguish between object and function. Reason being that I don't easily know the name of engine variables as the code is user generated.

switch (hostRefToScriptObj ) {
   case IList ar: 
     // array
  case ScriptObject so:
      if (so.PropertyNames.Any()) {
         // object
      }
      else {
         // function
      }
   default: // value
}

However, now I have another issue..

If I know I have a reference to a ScriptObject hostRefToScriptObj that is a function, how I can see the source? In javascript I'd call engineObject.toString() to get and parse the function source, but now I'm in C# with a host reference to a script object...

Can I pass that back to the engine somehow to toString? If I call hostRefToScriptObj.ToString() in C# I just get "Microsoft.ClearScript.V8.V8ScriptItem" Should I work out the whole path towards that object from engine.Global and call engine.Evaluate(wholePath + ".toString()")? Or can I pass these refs back to the engine somehow?

ClearScriptLib commented 1 year ago

Hi @gjvdkamp,

if (so.PropertyNames.Any())

Script functions, like other script objects, can have arbitrary user-defined properties, so this test is fragile. The instanceof Function test is more reliable, but even that could break if a script clobbered the built-in Function object.

The best way would be to save off Function before running any untrusted script code, then use that saved value for the instanceof test. We'll add a more convenient alternative in a future release.

Can I pass that back to the engine somehow to toString?

Of course:

var value = engine.Evaluate("(function (x) { return 1 / Math.sqrt(x) })");
if (value is ScriptObject scriptObject) {
    Console.WriteLine(scriptObject.InvokeMethod("toString"));
}

Or, alternatively:

dynamic value = engine.Evaluate("(function (x) { return 1 / Math.sqrt(x) })");
if (value is ScriptObject) {
    Console.WriteLine(value.toString());
}

Cheers!

gjvdkamp commented 1 year ago

That worked!! This stuff with dynamic is so elegant.. learning so much thanks!

And yes, did think of functions having properties (silly javascript..) thought it would be ok for now.. will take a new swing at it with my newfound prowess and after reading it again.