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

ClearScript execution of VBS code does not support ByRef parameter passing #511

Closed Rison-Hub closed 1 year ago

Rison-Hub commented 1 year ago

VBS CODE: Option Explicit Sub Main dim reftest reftest=0 zp.testRef 5,reftest zp.print reftest

End Sub Main VB.NET CODE: Public Class VBClass Public Sub Print(ByVal OutStr As Object, ByVal Optional iShow As Integer = 1) Console.WriteLine("OutStr: " & OutStr & "!") End Sub

Public Sub testRef(ByVal OutStr As Integer, Optional ByRef iShow As Integer= 0)
    iShow = iShow + OutStr
    Console.WriteLine("testRef: " & iShow & "!")
End Sub

End Class

C# Code: private void button10_Click(object sender, EventArgs e) { var VBscript = new VBClass(); using (var engine = new VBScriptEngine()) { engine.AddHostObject("zp", VBscript); var scriptCode = File.ReadAllText("D:/Script/abc.vbs"); engine.Execute(scriptCode);

        }

    }
ClearScriptLib commented 1 year ago

Hello @Rison-Hub,

When a script invokes managed code, ClearScript uses the C# compiler to select the correct method – or construct a suitable generic method – based on the call signature. Unlike Visual Basic and VBScript, C# forces the caller to specify the passing mechanism for each argument.

Because script languages usually give you no way to do that, ClearScript provides its own solution. To pass something by reference to a managed method, you must use a host variable. Here's an example based on your code above:

using (var engine = new VBScriptEngine()) {
    engine.AddHostObject("zp", new VBClass());
    engine.AddHostObject("host", new HostFunctions());
    engine.Execute(@"
        Option Explicit
        Sub Main
            dim reftest
            reftest = host.newVar(clng(0))
            zp.testRef 5, reftest.ref
            zp.print reftest
        End Sub
        Main
    ");
}

Good luck!

Rison-Hub commented 1 year ago

Executing the code snippet you provided will result in an exception: "The most suitable overload method for 'VBLibrary.VBClass.testRef(int, ref object)' has some invalid arguments."

ClearScriptLib commented 1 year ago

Executing the code snippet you provided will result in an exception: "The most suitable overload method for 'VBLibrary.VBClass.testRef(int, ref object)' has some invalid arguments."

Oops, sorry. We had modified testRef slightly. The following should work with your original definition:

using (var engine = new VBScriptEngine()) {
    engine.AddHostObject("zp", new VBClass());
    engine.AddHostObject("host", new HostFunctions());
    engine.AddHostType(typeof(object));
    engine.Execute(@"
        Option Explicit
        Sub Main
            dim reftest
            reftest = host.newVar(object, clng(0))
            zp.testRef 5, reftest.ref
            zp.print reftest
        End Sub
        Main
    ");
}
Rison-Hub commented 1 year ago

Thank you for your suggestion, but there is another question. My second parameter in the VB.NET method is an optional parameter. If I want to pass only one argument, how can I achieve a ByRef parameter that is not specified and request the method? I made a small adjustment to the VB.NET method


Public Sub testRef(ByVal OutStr As Integer, Optional ByRef iShow As Integer = 0)
iShow = iShow + OutStr
Console.WriteLine("testRef: " & iShow & "!")
End Sub
C# code

using (var engine = new VBScriptEngine()) { engine.AddHostObject("zp", new VBClass()); engine.AddHostObject("host", new HostFunctions()); engine.Execute(@" Option Explicit Sub Main dim reftest zp.testRef 5 End Sub Main "); }


Error message: Microsoft.ClearScript.ScriptEngineException: "The method 'testRef' does not have an overload that takes '1' parameter."
ClearScriptLib commented 1 year ago

Hi again,

Error message: Microsoft.ClearScript.ScriptEngineException: "The method 'testRef' does not have an overload that takes '1' parameter."

Yes, unfortunately, C# doesn't support optional by-reference parameters, and, as we discussed above, ClearScript uses C#'s invocation semantics.

One workaround is to add an overload without the optional parameter:

Public Sub testRef(ByVal OutStr As Integer)
    testRef(OutStr, 0)
End Sub
Public Sub testRef(ByVal OutStr As Integer, Optional ByRef iShow As Integer = 0)
    iShow = iShow + OutStr
    Console.WriteLine("testRef: " & iShow & "!")
End Sub

Cheers!

Rison-Hub commented 1 year ago

Thank you very much for your guidance

ClearScriptLib commented 1 year ago

Please reopen this issue if you have additional questions about this topic. Thank you!

Rison-Hub commented 1 year ago

Please reopen this issue if you have additional questions about this topic. Thank you!

@ClearScriptLib I have encountered another issue. I want to pass a two-dimensional array and process it in VB.NET. How can I call and output the processed result values in VBS?

VBS code

Option Explicit
dim timeGap(1,1)
     timeGap(0,0)=37      
     timeGap(0,1)=1   
     timeGap(1,0)=39
     timeGap(1,1)=2

Sub Main
    dim ByReftest
       ByReftest = host.newVar(object,timeGap)
       zp.testArr 5, ByReftest.ref
      zp.print ByReftest(0,0)
End Sub
Main

VB.NET Code

Public Sub Print(ByVal strInfo As Object)
        Console.WriteLine("strInfo : " & strInfo & "!")
    End Sub

Public Sub testArr(ByVal Ipar As Integer, ByRef arr As Object)
       arr(0, 0)=arr(0, 0)+Ipar 
        Console.WriteLine("testRef: " & arr(0, 0) & "!")
    End Sub

C# Code

using (var engine = new VBScriptEngine())
            {
                engine.AddHostObject("host", new HostFunctions());
                engine.AddHostObject("zp", VBscript);
                engine.AddHostType(typeof(object));
                var scriptCode = File.ReadAllText("D:/Script/abc.vbs");
                engine.Execute(scriptCode);

            }

Executing zp.print ByReftest(0,0) throws an error: Microsoft.ClearScript.ScriptEngineException: "The object does not support the requested invocation operation."

ClearScriptLib commented 1 year ago

Hi @Rison-Hub,

Executing zp.print ByReftest(0,0) throws an error: Microsoft.ClearScript.ScriptEngineException: "The object does not support the requested invocation operation."

Hmm. ByRefTest is a host variable holding a .NET array (converted from a VBScript array). So, it's essentially a .NET array. Sadly, ClearScript doesn't support multidimensional indexing for .NET arrays via native VBScript syntax. It's a scenario that hasn't come up before, probably because most ClearScript users work with JavaScript, which has no native support for multidimensional arrays. We'll take a closer look at this for a future release. Thanks!

In the meantime, in this particular case, there's no need to use a host variable at all. Here's a working sample:

Public Sub Print(strInfo As Object)
    Console.WriteLine("strInfo : " & strInfo & "!")
End Sub
Public Sub testArr(Ipar As Integer, arr As Object)
    arr(0, 0) = arr(0, 0) + Ipar
    Console.WriteLine("testRef: " & arr(0, 0) & "!")
End Sub

C# code:

engine.AddHostObject("zp", new VBClass());
engine.Execute(@"
    Option Explicit
    dim timeGap(1,1)
        timeGap(0,0)=37      
        timeGap(0,1)=1   
        timeGap(1,0)=39
        timeGap(1,1)=2
    Sub Main
        zp.testArr 5, timeGap
        zp.print timeGap(0,0)
    End Sub
    Main
");

Please let us know if this solution works for you.

Thanks again!

Rison-Hub commented 1 year ago

Hi @Rison-Hub,

Executing zp.print ByReftest(0,0) throws an error: Microsoft.ClearScript.ScriptEngineException: "The object does not support the requested invocation operation."

Hmm. ByRefTest is a host variable holding a .NET array (converted from a VBScript array). So, it's essentially a .NET array. Sadly, ClearScript doesn't support multidimensional indexing for .NET arrays via native VBScript syntax. It's a scenario that hasn't come up before, probably because most ClearScript users work with JavaScript, which has no native support for multidimensional arrays. We'll take a closer look at this for a future release. Thanks!

In the meantime, in this particular case, there's no need to use a host variable at all. Here's a working sample:

Public Sub Print(strInfo As Object)
    Console.WriteLine("strInfo : " & strInfo & "!")
End Sub
Public Sub testArr(Ipar As Integer, arr As Object)
    arr(0, 0) = arr(0, 0) + Ipar
    Console.WriteLine("testRef: " & arr(0, 0) & "!")
End Sub

C# code:

engine.AddHostObject("zp", new VBClass());
engine.Execute(@"
    Option Explicit
    dim timeGap(1,1)
        timeGap(0,0)=37      
        timeGap(0,1)=1   
        timeGap(1,0)=39
        timeGap(1,1)=2
    Sub Main
        zp.testArr 5, timeGap
        zp.print timeGap(0,0)
    End Sub
    Main
");

Please let us know if this solution works for you.

Thanks again!

Thank you again for your guidance.

Rison-Hub commented 1 year ago

@ClearScriptLib I made a slight modification to the format and noticed another issue VB Code

Public Sub Print(strInfo As Object)
    Console.WriteLine("strInfo : " & strInfo & "!")
End Sub
    Public Sub testByVal(ByVal OutStr As Integer, ByVal iShow As Object)
        iShow = iShow + 5
        Console.WriteLine("testByVal: " & iShow & "!")
    End Sub
Public Sub testArr(Ipar As Integer, arr As Object)
    arr(0, 0) = arr(0, 0) + Ipar
    Console.WriteLine("testRef: " & arr(0, 0) & "!")
End Sub

C# Code

engine.AddHostObject("host", new HostFunctions());
 engine.AddHostObject("zp", VBscript);
engine.AddHostType(typeof(object));
engine.Execute(@"
    Option Explicit
    dim ByValtest
    ByValtest=3
    Sub Main
        zp.testByVal 5, ByValtest
        zp.print ByValtest
    End Sub
    Main
");

output : 8 3 but I would like it output 8 8

ClearScriptLib commented 1 year ago

Hi @Rison-Hub,

Because iShow is a by-value parameter, testByVal can't change the external variable that provides iShow's argument. For that, you must use a by-reference parameter:

Public Sub testByRef(ByVal OutStr As Integer, ByRef iShow As Object)
    iShow = iShow + 5
    Console.WriteLine("testByRef: " & iShow & "!")
End Sub

And then:

engine.Execute(@"
    Option Explicit
    dim ByReftest
    ByReftest = host.newVar(object, 3)
    Sub Main
        zp.testByRef 5, ByReftest.ref
        zp.print ByReftest
    End Sub
    Main
");

Output:

testByRef: 8!
strInfo : 8!

Note that the situation above with the array is different because .NET arrays are reference types, meaning that you can modify the contents of a .NET array without modifying the variable that holds a reference to it.

In this case, even though the Object type forces the integer argument to be boxed and passed by reference, boxed values are immutable, so iShow = iShow + 5 creates a new boxed value instead of modifying the one to which ByValtest refers.

Good luck!

Rison-Hub commented 1 year ago

engine.AddHostObject("host", new HostFunctions());

@ClearScriptLib I need to integrate with HALCON, where HObject is a reference type. VB Code

Public Function EmptyHObj() As Object Dim image As HObject = Nothing HOperatorSet.GenEmptyObj(image) Return image End Function

'Read Image Public Sub New_ReadImage(ByVal Template_path As String, ByVal image As Object) Dim ho_Image As HObject = Nothing Dim hv_Template_path As HTuple = Nothing hv_Template_path = New HTuple(Template_path) HOperatorSet.ReadImage(image , hv_Template_path) End Sub

'Display Image Public Sub display_open(ByVal Image As Object) Dim HWindow As HWindowControl = frm_open.HWindowControl1 Dim Window As HTuple = HWindow.HalconWindow HWindow.HalconWindow.ClearWindow() Dim hv_Width As HTuple = Nothing Dim hv_Height As HTuple = Nothing If IsNothing(ho_Image) = False AndAlso ho_Image.IsInitialized Then HOperatorSet.GetImageSize(ho_Image, hv_Width, hv_Height) HOperatorSet.SetPart(Window, 0, 0, hv_Height, hv_Width) HOperatorSet.DispObj(ho_Image, Window) End If hv_Width = Nothing hv_Height = Nothing End Sub

C# Code

engine.AddHostObject("host", new HostFunctions()); engine.AddHostObject("zp", VBscript); engine.AddHostType(typeof(object)); engine.Execute(@" Option Explicit Sub Main dim Image_Path,Image Image_Path="D:\AUTO Test System\Write\Image_Correction\MB2701-A1\Image\0.bmp" Image=zp.EmptyHObj() call zp.New_ReadImage(Image_Path,Image) call zp.display_open(Image) End Sub Main ");

The value passed to display_open is the return value of EmptyHObj, not the value read from New_ReadImage.

However, if you use the following code, the value passed is correct.

VB Code

Public Function EmptyHObj() As Object Dim image As HObject = Nothing HOperatorSet.GenEmptyObj(image) Return image End Function

'Read Image Public Sub New_ReadImage(ByVal Template_path As String, ByRef image As Object) Dim ho_Image As HObject = Nothing Dim hv_Template_path As HTuple = Nothing hv_Template_path = New HTuple(Template_path) HOperatorSet.ReadImage(image , hv_Template_path) End Sub

'Display Image Public Sub display_open(ByVal Image As Object) Dim HWindow As HWindowControl = frm_open.HWindowControl1 Dim Window As HTuple = HWindow.HalconWindow HWindow.HalconWindow.ClearWindow() Dim hv_Width As HTuple = Nothing Dim hv_Height As HTuple = Nothing If IsNothing(ho_Image) = False AndAlso ho_Image.IsInitialized Then HOperatorSet.GetImageSize(ho_Image, hv_Width, hv_Height) HOperatorSet.SetPart(Window, 0, 0, hv_Height, hv_Width) HOperatorSet.DispObj(ho_Image, Window) End If hv_Width = Nothing hv_Height = Nothing End Sub

C# Code

engine.AddHostObject("host", new HostFunctions()); engine.AddHostObject("zp", VBscript); engine.AddHostType(typeof(object)); engine.Execute(@" Option Explicit Sub Main dim Image_Path,Image Image_Path="D:\AUTO Test System\Write\Image_Correction\MB2701-A1\Image\0.bmp" Image=host.newVar(object, zp.EmptyHObj()) call zp.New_ReadImage(Image_Path,Image.ref) call zp.display_open(Image) End Sub Main ");

ClearScriptLib commented 1 year ago

Hi @ClearScriptLib,

The behavior you're seeing is correct.

According to the HALCON documentation, the ReadImage method looks like this (in C#):

static void ReadImage(out HObject image, HTuple fileName)

Note that image is an out – or output – parameter. At the .NET level, that's just a by-reference parameter. However, in C#, out – unlike ref – additionally specifies that the method ignores any passed-in value and replaces it on successful return.

In this case, ReadImage creates a new object and stores a reference to it in image. If some other object is passed in, the method ignores it. It does not modify it or operate on it in any way.

Therefore, you don't have to call GenEmptyObj. However, because Main needs the new object, New_ReadImage must either store it in a by-reference parameter or simply return it.

Cheers!

Rison-Hub commented 1 year ago

Hi @ClearScriptLib,

The behavior you're seeing is correct.

According to the HALCON documentation, the ReadImage method looks like this (in C#):

static void ReadImage(out HObject image, HTuple fileName)

Note that image is an out – or output – parameter. At the .NET level, that's just a by-reference parameter. However, in C#, out – unlike ref – additionally specifies that the method ignores any passed-in value and replaces it on successful return.

In this case, ReadImage creates a new object and stores a reference to it in image. If some other object is passed in, the method ignores it. It does not modify it or operate on it in any way.

Therefore, you don't have to call GenEmptyObj. However, because Main needs the new object, New_ReadImage must either store it in a by-reference parameter or simply return it.

Cheers!

Thank you again for your guidance.