[PATCH v5 0/1] MR10600: vbscript: Convert VT_DISPATCH arguments to string in Eval/Execute/ExecuteGlobal.
For VT_DISPATCH arguments, call the object's default property (DISPID_VALUE) to get a string value instead of returning the object via VariantCopy (Eval) or returning type mismatch (Execute/ ExecuteGlobal). This matches Windows behavior where passing a dispatch object without a default property fails with error 438, and objects with a default property have it invoked to obtain the string to evaluate or execute. -- v5: vbscript: Convert VT_DISPATCH arguments to string in Eval/Execute/ExecuteGlobal. https://gitlab.winehq.org/wine/wine/-/merge_requests/10600
From: Francis De Brabandere <francisdb@gmail.com> For VT_DISPATCH arguments, call the object's default property (DISPID_VALUE) to get a string value instead of returning the object via VariantCopy (Eval) or returning type mismatch (Execute/ ExecuteGlobal). This matches Windows behavior where passing a dispatch object without a default property fails with error 438, and objects with a default property have it invoked to obtain the string to evaluate or execute. --- dlls/vbscript/global.c | 64 ++++++++++++++++++++++++--- dlls/vbscript/tests/lang.vbs | 84 ++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 6 deletions(-) diff --git a/dlls/vbscript/global.c b/dlls/vbscript/global.c index 23a5f235c85..d739a274f5d 100644 --- a/dlls/vbscript/global.c +++ b/dlls/vbscript/global.c @@ -3681,21 +3681,51 @@ static HRESULT Global_Unescape(BuiltinDisp *This, VARIANT *arg, unsigned args_cn return S_OK; } +static HRESULT dispatch_to_string(script_ctx_t *ctx, IDispatch *disp, BSTR *ret) +{ + DISPPARAMS dp = {0}; + VARIANT v; + HRESULT hres; + + if(!disp) + return MAKE_VBSERROR(VBSE_OBJECT_VARIABLE_NOT_SET); + hres = disp_call(ctx, disp, DISPID_VALUE, &dp, &v); + if(FAILED(hres)) + return hres; + if(V_VT(&v) == VT_BSTR) { + *ret = V_BSTR(&v); + return S_OK; + } + hres = to_string(&v, ret); + VariantClear(&v); + return hres; +} + static HRESULT Global_Eval(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) { + BSTR conv_str = NULL; vbscode_t *code; + BSTR str; HRESULT hres; TRACE("%s\n", debugstr_variant(arg)); - if(V_VT(arg) != VT_BSTR) { + if(V_VT(arg) == VT_BSTR) { + str = V_BSTR(arg); + }else if(V_VT(arg) == VT_DISPATCH) { + hres = dispatch_to_string(This->ctx, V_DISPATCH(arg), &conv_str); + if(FAILED(hres)) + return hres; + str = conv_str; + }else { if(res) return VariantCopy(res, arg); return S_OK; } - hres = compile_script(This->ctx, V_BSTR(arg), NULL, NULL, 0, 0, + hres = compile_script(This->ctx, str, NULL, NULL, 0, 0, SCRIPTTEXT_ISEXPRESSION, FALSE, &code); + SysFreeString(conv_str); if(FAILED(hres)) { clear_error_loc(This->ctx); return hres; @@ -3711,16 +3741,27 @@ static HRESULT Global_Eval(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, V static HRESULT Global_Execute(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) { + BSTR conv_str = NULL; vbscode_t *code; + BSTR str; HRESULT hres; TRACE("%s\n", debugstr_variant(arg)); - if(V_VT(arg) != VT_BSTR) + if(V_VT(arg) == VT_BSTR) { + str = V_BSTR(arg); + }else if(V_VT(arg) == VT_DISPATCH) { + hres = dispatch_to_string(This->ctx, V_DISPATCH(arg), &conv_str); + if(FAILED(hres)) + return hres; + str = conv_str; + }else { return MAKE_VBSERROR(VBSE_TYPE_MISMATCH); + } - hres = compile_script(This->ctx, V_BSTR(arg), NULL, NULL, 0, 0, + hres = compile_script(This->ctx, str, NULL, NULL, 0, 0, 0, TRUE, &code); + SysFreeString(conv_str); if(FAILED(hres)) { clear_error_loc(This->ctx); return hres; @@ -3746,16 +3787,27 @@ static HRESULT Global_Execute(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt static HRESULT Global_ExecuteGlobal(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) { + BSTR conv_str = NULL; vbscode_t *code; + BSTR str; HRESULT hres; TRACE("%s\n", debugstr_variant(arg)); - if(V_VT(arg) != VT_BSTR) + if(V_VT(arg) == VT_BSTR) { + str = V_BSTR(arg); + }else if(V_VT(arg) == VT_DISPATCH) { + hres = dispatch_to_string(This->ctx, V_DISPATCH(arg), &conv_str); + if(FAILED(hres)) + return hres; + str = conv_str; + }else { return MAKE_VBSERROR(VBSE_TYPE_MISMATCH); + } - hres = compile_script(This->ctx, V_BSTR(arg), NULL, NULL, 0, 0, + hres = compile_script(This->ctx, str, NULL, NULL, 0, 0, 0, TRUE, &code); + SysFreeString(conv_str); if(FAILED(hres)) { clear_error_loc(This->ctx); return hres; diff --git a/dlls/vbscript/tests/lang.vbs b/dlls/vbscript/tests/lang.vbs index d59f05c54a0..68a7b5b35dd 100644 --- a/dlls/vbscript/tests/lang.vbs +++ b/dlls/vbscript/tests/lang.vbs @@ -3104,6 +3104,90 @@ End Sub Call TestEvalNoLeak Call ok(getVT(evalLocal) = "VT_EMPTY*", "evalLocal leaked from Eval scope: " & getVT(evalLocal)) +' Eval with non-BSTR arguments: non-string values are returned as-is +Call ok(Eval(42) = 42, "Eval(42) = " & Eval(42)) +Call ok(getVT(Eval(42)) = "VT_I2", "Eval(42) type = " & getVT(Eval(42))) +Call ok(Eval(3.14) = 3.14, "Eval(3.14) = " & Eval(3.14)) +Call ok(Eval(True) = True, "Eval(True) = " & Eval(True)) +Call ok(IsNull(Eval(Null)), "Eval(Null) should be Null") +Call ok(IsEmpty(Eval(Empty)), "Eval(Empty) should be Empty") + +' Eval with object that has a default property: converts to string via default, then evaluates +Class EvalHasDefault + Public Default Function GetValue() + GetValue = "True" + End Function +End Class +Dim evalHasDefaultObj +Set evalHasDefaultObj = New EvalHasDefault +Call ok(Eval(evalHasDefaultObj) = True, "Eval(HasDefault) = " & Eval(evalHasDefaultObj)) + +' Eval with object without default property: should fail with error 438 +Class EvalNoDefault + Public Name +End Class +Dim evalNoDefaultObj +Set evalNoDefaultObj = New EvalNoDefault + +On Error Resume Next +Err.Clear +Dim evalObjResult +evalObjResult = Eval(evalNoDefaultObj) +Call ok(Err.Number = 438, "Eval(NoDefault) err.number = " & Err.Number & " expected 438") + +' Eval(Nothing) should fail with error 91 +Err.Clear +Dim evalNothingResult +evalNothingResult = Eval(Nothing) +Call ok(Err.Number = 91, "Eval(Nothing) err.number = " & Err.Number & " expected 91") + +' Eval(dispatch) in If..Is should trigger error, not silently return the object +Err.Clear +Dim evalCheckResult +evalCheckResult = True +If Not Eval(evalNoDefaultObj) Is evalNoDefaultObj Or Err Then evalCheckResult = False +Call ok(evalCheckResult = False, "Eval(obj) Is obj: checkResult = " & evalCheckResult & " expected False") + +' Execute/ExecuteGlobal with non-BSTR should fail with type mismatch +Err.Clear +Execute 42 +Call ok(Err.Number = 13, "Execute(42) err.number = " & Err.Number & " expected 13") + +Err.Clear +ExecuteGlobal 42 +Call ok(Err.Number = 13, "ExecuteGlobal(42) err.number = " & Err.Number & " expected 13") + +' Execute/ExecuteGlobal with dispatch: converts via default property +Class ExecHasDefault + Public Default Function GetValue() + GetValue = "execHdResult = 99" + End Function +End Class +Dim execHdObj : Set execHdObj = New ExecHasDefault +Dim execHdResult : execHdResult = 0 + +Err.Clear +Execute execHdObj +Call ok(Err.Number = 0, "Execute(HasDefault) err.number = " & Err.Number) +Call ok(execHdResult = 99, "Execute(HasDefault) execHdResult = " & execHdResult) + +Err.Clear +execHdResult = 0 +ExecuteGlobal execHdObj +Call ok(Err.Number = 0, "ExecuteGlobal(HasDefault) err.number = " & Err.Number) +Call ok(execHdResult = 99, "ExecuteGlobal(HasDefault) execHdResult = " & execHdResult) + +' Execute/ExecuteGlobal with dispatch without default property: error 438 +Err.Clear +Execute evalNoDefaultObj +Call ok(Err.Number = 438, "Execute(NoDefault) err.number = " & Err.Number & " expected 438") + +Err.Clear +ExecuteGlobal evalNoDefaultObj +Call ok(Err.Number = 438, "ExecuteGlobal(NoDefault) err.number = " & Err.Number & " expected 438") + +On Error GoTo 0 + ' ExecuteGlobal tests x = 0 ExecuteGlobal "x = 42" -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10600
Jacek Caban (@jacek) commented about dlls/vbscript/global.c:
{ + BSTR conv_str = NULL; vbscode_t *code; + BSTR str; HRESULT hres;
TRACE("%s\n", debugstr_variant(arg));
- if(V_VT(arg) != VT_BSTR) { + if(V_VT(arg) == VT_BSTR) { + str = V_BSTR(arg); + }else if(V_VT(arg) == VT_DISPATCH) { + hres = dispatch_to_string(This->ctx, V_DISPATCH(arg), &conv_str); + if(FAILED(hres)) + return hres; + str = conv_str; I wonder if we should special-case VT_DISPATCH here. Most vbscript code simply uses `to_string`, which should work here for other argument types as well. `VariantCopy` is obviously faster, but as this case shows, it is not correct for corner cases (and probably not a popular code path anyway).
-- https://gitlab.winehq.org/wine/wine/-/merge_requests/10600#note_136765
On Fri Apr 17 14:12:56 2026 +0000, Jacek Caban wrote:
I wonder if we should special-case VT_DISPATCH here. Most vbscript code simply uses `to_string`, which should work here for other argument types as well. `VariantCopy` is obviously faster, but as this case shows, it is not correct for corner cases (and probably not a popular code path anyway). Tested on Windows, VariantCopy in Eval is intentional. Non-BSTR/non-DISPATCH args are preserved verbatim (including types to_string can't convert, like arrays and Null), while Execute/ExecuteGlobal uniformly return err 13 for the same types (not a to_string path either). Only VT_DISPATCH invokes DISPID_VALUE in both.
Added tests for Eval(Array) pass-through and Execute Null/Array/True → err 13 to lock this in. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10600#note_136769
This merge request was approved by Jacek Caban. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10600
participants (3)
-
Francis De Brabandere -
Francis De Brabandere (@francisdb) -
Jacek Caban (@jacek)