Skip to content

Fix x64 cross-apartment marshaling of IScriptError (Error cast -> E_NOINTERFACE)#36

Open
hugo-fc-visions wants to merge 1 commit into
tablacus:masterfrom
hugo-fc-visions:fix/iscripterror-typelib-marshaling
Open

Fix x64 cross-apartment marshaling of IScriptError (Error cast -> E_NOINTERFACE)#36
hugo-fc-visions wants to merge 1 commit into
tablacus:masterfrom
hugo-fc-visions:fix/iscripterror-typelib-marshaling

Conversation

@hugo-fc-visions

@hugo-fc-visions hugo-fc-visions commented Jul 20, 2026

Copy link
Copy Markdown

On x64, casting ScriptControl.Error to the strongly-typed IScriptError/Error interface fails with E_NOINTERFACE when the object is consumed from a COM MTA thread (e.g. a .NET host whose main thread is MTA):

Unable to cast COM object ... to interface type 'IScriptError' ...
QueryInterface ... IID '{70841C78-067D-11D0-95D8-00A02463AB28}' ...
E_NOINTERFACE (0x80004002)

Root cause: the control is registered ThreadingModel=Apartment (STA). When it is created from an MTA thread, COM returns a proxy, and QueryInterface on that proxy for the IScriptError vtable interface needs marshaling metadata. The type library only declared IScriptControl, never IScriptError, so there was no proxy/stub nor type-library marshaler registered for {70841C78-...} on x64 and the cross-apartment QI failed. (Late-bound/IDispatch access still works because IDispatch is always marshalable; only the strongly-typed vtable cast breaks.)

Fix: declare IScriptError in the .idl (dual + oleautomation, with the exact vtable order and dispids CTScriptError already implements) so it becomes part of the type library. The existing DllRegisterServer -> RegisterTypeLib call then registers an oleautomation marshaler for the interface, and the cross-apartment QueryInterface (and the managed cast) succeed.

No C++ change: CTScriptError already implements the interface (it uses the #import-ed IScriptError, same IID); tsc64.cpp does not include the MIDL header, so there is no type clash.

Code to reproduce using .net 10 console app:

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

Type scTypeFromPID = Type.GetTypeFromProgID("ScriptControl");
dynamic mssc = Activator.CreateInstance(scTypeFromPID);
mssc.Language = "VBScript";
mssc.ExecuteStatement("lTest = 1");
IScriptError erro = mssc.Error; // throws here

[ComImport]
[TypeLibType(4304)]
[Guid("70841C78-067D-11D0-95D8-00A02463AB28")]
public interface IScriptError
{
    [DispId(201)]
    int Number
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(201)]
        get;
    }

    [DispId(202)]
    string Source
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(202)]
        [return: MarshalAs(UnmanagedType.BStr)]
        get;
    }

    [DispId(203)]
    string Description
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(203)]
        [return: MarshalAs(UnmanagedType.BStr)]
        get;
    }

    [DispId(204)]
    string HelpFile
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(204)]
        [return: MarshalAs(UnmanagedType.BStr)]
        get;
    }

    [DispId(205)]
    int HelpContext
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(205)]
        get;
    }

    [DispId(-517)]
    string Text
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(-517)]
        [return: MarshalAs(UnmanagedType.BStr)]
        get;
    }

    [DispId(206)]
    int Line
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(206)]
        get;
    }

    [DispId(-529)]
    int Column
    {
        [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        [DispId(-529)]
        get;
    }

    [MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    [DispId(208)]
    void Clear();
}

…OINTERFACE)

On x64, casting ScriptControl.Error to the strongly-typed IScriptError/Error
interface fails with E_NOINTERFACE when the object is consumed from a COM MTA
thread (e.g. a .NET host whose main thread is MTA):

  Unable to cast COM object ... to interface type 'MSScriptControl.Error' ...
  QueryInterface ... IID '{70841C78-067D-11D0-95D8-00A02463AB28}' ...
  E_NOINTERFACE (0x80004002)

Root cause: the control is registered ThreadingModel=Apartment (STA). When it
is created from an MTA thread, COM returns a proxy, and QueryInterface on that
proxy for the IScriptError vtable interface needs marshaling metadata. The
type library only declared IScriptControl, never IScriptError, so there was no
proxy/stub nor type-library marshaler registered for {70841C78-...} on x64 and
the cross-apartment QI failed. (Late-bound/IDispatch access still works because
IDispatch is always marshalable; only the strongly-typed vtable cast breaks.)

Fix: declare IScriptError in the .idl (dual + oleautomation, with the exact
vtable order and dispids CTScriptError already implements) so it becomes part
of the type library. The existing DllRegisterServer -> RegisterTypeLib call
then registers an oleautomation marshaler for the interface, and the
cross-apartment QueryInterface (and the managed cast) succeed.

No C++ change: CTScriptError already implements the interface (it uses the
#import-ed IScriptError, same IID); tsc64.cpp does not include the MIDL header,
so there is no type clash.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant