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

Possible bug in "FOR EACH item IN list" loop ? #594

Closed juan-nxlevel closed 2 months ago

juan-nxlevel commented 2 months ago

Dear ClearScript,

I have this VBS: dim allPages set allPages = page.getElementsByTagName("page[@listed='true']") Console.Log "Count: " & allPages.length for each p in allPages Console.Write p.toString() next

Which results in this:

"Count: 41" "Error: The object is not enumerable"

Note that the "page" object was instantiated via CreateObject("Microsoft.XMLDOM") with 41 nodes, so it's not a .NET object that inherited Enumerable. It appears that your FOR EACH x IN y cannot handle lists/objects that came from vbs? Is this something you need to implement or perhaps is there a switch to allow the "for each" loops for this type of lists to work?

Thanks any advice!

ClearScriptLib commented 2 months ago

HI @juan-nxlevel,

This is very unlikely to be a ClearScript issue, as ClearScript isn't involved in VBScript execution beyond invoking the interpreter. Nor is it involved in the CreateObject or Microsoft.XMLDOM implementation.

That said, the following works correctly in our testing:

using var engine = new VBScriptEngine();
engine.AddHostType(typeof(Console));
engine.Script.xml = @"
    <document>
        <page id=""123"" listed=""true""/>
        <page id=""456""/>
        <page id=""789""/>
        <page id=""987"" listed=""true""/>
        <page id=""654""/>
        <page id=""321""/>
        <page id=""135"" listed=""true""/>
        <page id=""246""/>
        <page id=""357""/>
        <page id=""468"" listed=""true""/>
        <page id=""579""/>
    </document>
";
engine.Execute(@"
    set document = CreateObject(""Microsoft.XMLDOM"")
    document.LoadXML(xml)
    set allPages = document.getElementsByTagName(""page[@listed='true']"")
    for each page in allPages
        Console.WriteLine page.getAttribute(""id"")
    next
");

Output:

123
987
135
468

Can you think of something you're doing differently?

juan-nxlevel commented 2 months ago

Thank you for the quick feedback. I am trying to convert an app that is using MSScriptControl to use ClearScript instead. I traced down the issue to the origin of "page" in my example. It turns out that this object is not created in VBS as in your example. That object was actually created in VB.NET like this:

Dim obj As Object = CreateObject("Microsoft.XMLDOM")

and then later on passed to ClearScript, like this:

Script.AddHostObject("page", HostItemFlags.GlobalMembers, obj)

I also tried adding a reference to the project to Microsoft MSXML2 to see if early binding would help, so I tested this:

Dim testPage As MSXML2.DOMDocument60 = New MSXML2.DOMDocument60() testPage.loadXML(\"<page id=\"a\">A<page id=\"b\">B\") Script.AddHostObject("page", HostItemFlags.GlobalMembers, testPage)

Which results in this:

"Count: 2" "Error: The object is not enumerable"

This means that somewhere along the road of "AddHostObject" the XML object lost its Enumerability.

Thank you for any suggestions !

ClearScriptLib commented 2 months ago

Hi @juan-nxlevel,

Thanks for providing additional information. We've taken a deeper look.

You're exposing a .NET-wrapped COM object (RCW) to the script engine. There's nothing wrong with that, but COM Interop has bugs and limitations, especially on cross-platform .NET (as opposed to the Windows-only .NET Framework).

With this particular object, we found what looks like a COM Interop bug that can affect enumerability in ClearScript, and we'll provide a workaround in the next release. Unfortunately, there are other issues – ones that ClearScript cannot work around – that prevent your example from working on all .NET platforms.

On the other hand, JScript and VBScript support COM natively. If you aren't overriding or extending XMLDOM functionality, you could let the script engine access the object directly – that is, without ClearScript's involvement. Not only would that remove COM Interop from the picture, but it would also greatly improve the performance of XMLDOM access from script code.

Here's a VB sample that demonstrates the technique. Note the use of HostItemFlags.DirectAccess:

Using engine As New VBScriptEngine()
    Dim document = CreateObject("Microsoft.XMLDOM")
    document.loadXML("
        <document>
            <page id=""123"" listed=""true""/>
            <page id=""456""/>
            <page id=""789""/>
            <page id=""987"" listed=""true""/>
            <page id=""654""/>
            <page id=""321""/>
            <page id=""135"" listed=""true""/>
            <page id=""246""/>
            <page id=""357""/>
            <page id=""468"" listed=""true""/>
            <page id=""579""/>
        </document>
    ")
    engine.AddHostObject("document", HostItemFlags.DirectAccess, document)
    engine.AddHostType(GetType(Console))
    engine.Execute("
        set allPages = document.getElementsByTagName(""page[@listed='true']"")
        for each page in allPages
            Console.WriteLine page.getAttribute(""id"")
        next
    ")

Please let us know if this approach works for you. Thanks!

juan-nxlevel commented 2 months ago

Dear ClearScript:

The flag HostItemFlags.DirectAccess did the trick!

Thank you very much for your prompt and clear responses!