.NET 4.8 - Wrtiting COM library with parameterized properties - c#

I am working on migrating legacy VB6 COM library that registered with regsvr32 and used primarily in vbscript and ASP Classic.
The new .NET 4.8 substitution library is registered with regasm.
All was good until I came across some features that I can not directly migrate because I have not enough knowledge in this field.
Legacy VB6 library has the following Typelibrary code (from OleView, reduced for convenience):
[id(0x00068), propget]
VARIANT_BOOL IncomeOverride([in] short index);
[id(0x00068), propput]
void IncomeOverride(
[in] short index,
[in] VARIANT_BOOL rhs
);
[id(0x00069), propget]
CY IncomeOverrideAmt([in] short index);
[id(0x00069), propput]
void IncomeOverrideAmt(
[in] short index,
[in] CY rhs
);
The caller is using these methods like this:
oTest.IncomeOverride(1) = True
oTest.IncomeOverrideAmt(1) = 12
Firstly I have tried to use properties.
But since there is no parameterized properties available in C#, I had to try out the methods like this:
[DispId(0x00068)]
bool IncomeOverride([In] short index);
[DispId(0x00068)]
void IncomeOverride([In] short index, [In] bool v);
[DispId(0x00069)]
decimal IncomeOverrideAmt([In] short index);
[DispId(0x00069)]
void IncomeOverrideAmt([In] short index, [In] decimal v);
That compiles but does not work because regasm makes a warning on registering and refuses generating correct tlb. The warning as follows:
Type library exporter warning processing 'TestLibrary'. Warning: The
type had one or more duplicate DISPIDs specified. The duplicate
DISPIDs were ignored.
The produced typelibrary is far from what is needed:
[id(0x0001)]
VARIANT_BOOL IncomeOverride([in] short index);
[id(0x0002)]
void IncomeOverride_2(
[in] short index,
[in] VARIANT_BOOL v
);
[id(0x0003)]
CY IncomeOverrideAmt([in] short index);
[id(0x0004)]
void IncomeOverrideAmt_2(
[in] short index,
[in] CY v
);
Next iteration was to use indexers.
[DispId(0x00068)]
[System.Runtime.CompilerServices.IndexerName("IncomeOverride")]
bool this[short index]
{
[return: MarshalAs(UnmanagedType.Currency)]
get;
[param: In, MarshalAs(UnmanagedType.Currency)]
set;
}
[DispId(0x00069)]
[System.Runtime.CompilerServices.IndexerName("IncomeOverrideAmt")]
decimal this[short index]
{
[return: MarshalAs(UnmanagedType.Currency)]
get;
[param: In, MarshalAs(UnmanagedType.Currency)]
set;
}
That is actually a good solution but unfortunately it does not even compile because you can not have multiple indexers with different IndexerName values in a single interface/class.
The last attempt to try to accomodate such feature was to use properties that return arrays/objects with indexers:
[DispId(0x00068)]
bool[] IncomeOverride { get; }
[DispId(0x00069)]
decimal[] IncomeOverrideAmt { get; }
[DispId(0x00068)]
SomeClassWithBoolIndexer IncomeOverride { get; }
[DispId(0x00069)]
SomeClassWithDecimalIndexer IncomeOverrideAmt { get; }
That compiles and registers but since it is generating incorrect typelibrary - it can not be used in vbscript/VB6/ASP Classic.
So the question is - how to implement such feature?
Or just emulate?
Writing custom Typelibrary for C# library?
Is this even possible with the means of C#?
Thank in advance.
Update:
Found a similar question here: How to make C# COM class support parameterized properties from VB6
It actually makes most of the work done but unlike the author of that question - it does not work without using .Value for the parameterized property.
Trying to dig in and find out why VBscript does not recognize default property (Value). Unless this - all works like a champ!
But without default property recognition it is dead because requires rewriting tonns of legacy code... we currently do not need binary compat, just to make the late binding (vbscript) work.
Anyone has any idea why it is not working as supposed?
Update 2
Ok as for now two main solutions (apparently none of them working):
1. using PropertyAccessor class
VBScript:
Dim oTestObject: Set oTestObject = CreateObject("TestSharpLib.TestObject")
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]" 'THIS LINE WORKS
oTestObject.IncomeOverride(1).Value = "True" 'THIS LINE WORKS
oTestObject.IncomeOverride(1) = "True" 'THIS LINE DOES NOT WORK
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]"
C#
using System;
using System.Runtime.InteropServices;
using TestVBNET;
namespace TestSharpLib
{
[ComVisible(true)]
[ProgId("TestSharpLib.TestObject")]
[Guid("49067FAC-A1B3-4469-9032-4E958AA7381C")]
public class TestObject: : ITestObject
{
public object IncomeOverride(short index)
{
return new PropertyAccessor(this, index);
}
public class PropertyAccessor
{
TestObject _owner;
short _index;
public PropertyAccessor(TestObject owner, short index)
{
_owner = owner;
_index = index;
}
[DispId(0)]
public string Value {
get => _index.ToString();
set { }
}
}
}
[ComVisible(true)]
[Guid("4C0FDA39-1027-415A-968C-9A6DF9BEB499")]
public interface ITestObject
{
[DispId(1)]
object IncomeOverride(short index);
}
}
Seems like according to Erik Lippert you can not omit default properties in VBScript.
https://learn.microsoft.com/en-us/archive/blogs/ericlippert/vbscript-default-property-semantics
2. Using interface declared in VB.NET
VBScript:
Dim oTestObject: Set oTestObject = CreateObject("TestSharpLib.TestObject")
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]" 'THIS LINE NOT WORKING
oTestObject.IncomeOverride(1) = "True" 'THIS LINE NOT WORKING
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]"
C#
using System;
using System.Runtime.InteropServices;
using TestVBNET;
namespace TestSharpLib
{
[ComVisible(true)]
[ProgId("TestSharpLib.TestObject")]
[Guid("49067FAC-A1B3-4469-9032-4E958AA7381C")]
public class TestObject: IIncomeOverride
{
public string get_IncomeOverride(int index)
{
return index.ToString();
}
public void set_IncomeOverride(int index, string Value)
{
//throw new NotImplementedException();
}
}
}
VB.NET
Imports System.Runtime.InteropServices
<ComVisible(True)>
<Guid("F40144F0-6BA1-4983-B6EE-D593CA0A2F75")>
Public Interface IIncomeOverride
<DispId(1745027127)>
Property IncomeOverride(ByVal index As Integer) As String
End Interface
This solution does not work at all. Although regasm says the dll registered and OleView actually shows correct typelibrary - VBScript does not see IncomeOverride property at all.
c:\Users\Administrator\Desktop\test.vbs(2, 1) Microsoft VBScript runtime error: Object doesn't support this property or method: 'IncomeOverride'
Typelibrary:
[
uuid(f40144f0-6ba1-4983-b6ee-d593ca0a2f75),
dual,
oleautomation
]
interface IIncomeOverride : IDispatch#i {
[id(0x68030037), propget]
HRESULT IncomeOverride(
[in] long index,
[out, retval] BSTR* pRetVal
);
[id(0x68030037), propput]
HRESULT IncomeOverride(
[in] long index,
[in] BSTR pRetVal
);
};
[
uuid(f40144f0-6ba1-4983-b6ee-d593ca0a2f75),
dual
]
dispinterface IIncomeOverride {
methods:
**removed object methods***
[id(0x68030037), propget]
BSTR IncomeOverride([in] long index);
[id(0x68030037), propput]
void IncomeOverride(
[in] long index,
[in] BSTR rhs
);
};
So for now I am still digging in but out of new ideas.
Main disappointment was that VBScript does not see implemented (declared in VB.NET interface) properties/methods.

Related

Marshalling Function Pointers with .NET 7 LibraryImport

I'm trying to implement some P/Invoke code using the new LibraryImport attribute, as opposed to the old DllImport. Specifically, I am trying to marshal a WNDCLASSEXW struct for use in RegisterClassEx.
Here is a simplified, shortened version of my managed implementation of WNDCLASSEXW:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WindowClass
{
private uint StructSize;
public WindowClassStyle Style;
[MarshalAs(UnmanagedType.FunctionPtr)]
public Win32API.WindowProcedure? WindowProcedure;
private int ClassAdditionalBytes;
private int WindowAdditionalBytes;
public IntPtr Instance;
public IntPtr Icon;
public IntPtr Cursor;
public IntPtr BackgroundBrush;
[MarshalAs(UnmanagedType.LPWStr)]
public string? ClassMenuResourceName;
[MarshalAs(UnmanagedType.LPWStr)]
public string? ClassName;
public IntPtr SmallIcon;
}
And my definition of Win32API.WindowProcedure:
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate nint WindowProcedure(IntPtr windowHandle, MessageID messageID, nuint wParam, nint lParam);
And finally my definition of RegisterClassEx:
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
public static partial ushort RegisterClassEx(in WindowClass classDefinition);
However, this results in the error:
Error SYSLIB1051: The type 'xxx.WindowClass' is not supported by source-generated P/Invokes. The generated source will not handle marshalling of parameter 'classDefinition'.
Therefore I believe I require custom marshalling for the WindowClass struct.
However, with this system being relatively new, I'm having difficulty finding good guidance on how to do this correctly and optimally. Previously, DllImport would magically marshal most types with little guidance, but LibraryImport appears to require more information, and be a bit stricter.
I could circumvent the problem by changing the type to IntPtr and requiring conversion of the delegate to IntPtr elsewhere in the program, but I would much prefer to do it as close to the managed/unmanaged boundary as possible and keep the structs and exposed native functions usable with more descriptive types.
Some of the resources I have found while searching:
The old P/Invoke documentation regarding delegates/function pointers
The new information regarding CustomMarshaller
The design documentation for the new source generator-based system
Primary Question: How do I correctly implement custom marshalling for my WNDPROC and the LP(C)WSTRs?
Question 2:
I would prefer to use a readonly struct, and turn all of the members into { get; init; } properties instead of fields, due to the nicer semantics. However I've noticed that the MarshalAs attribute cannot be applied to properties. Is there a good way to both use readonly structs with properties, while also providing the necessary information to ensure everything gets marshalled in/out correctly? Specifically for more complex types such as string? <-> LPCWSTR, delegate? <-> void*, and other such types I may encounter.
Bonus Question:
It appears that LibraryImport de-emphasizes the importance of specifying the correct calling convention. It's no longer part of the main attribute like DllImport, instead using a secondary attribute that looks like this: [UnmanagedCallConv(CallConvs = new[] { typeof(CallConvStdcall) })] which frankly looks terrible. Is specifying calling convention necessary or beneficial now?
I was able to get it working with custom marshaling. While Simon's advice of changing the struct to contain the native types makes sense in the general case, in my case it doesn't, as these will be exposed for others to use.
The answer might be different for faster, more frequently called methods, but in this case registering a class and creating a window is inherently quite an expensive operation, so the added overhead of copying data to/from a different struct isn't worth any concern.
The marshaler was implemented like this:
[CustomMarshaller(typeof(WindowClass), MarshalMode.UnmanagedToManagedIn, typeof(WindowClassMarshaler))]
[CustomMarshaller(typeof(WindowClass), MarshalMode.ManagedToUnmanagedIn, typeof(ManagedToUnmanagedIn))]
internal static unsafe class WindowClassMarshaler
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal unsafe struct WindowClassUnmanaged
{
public uint StructSize;
public uint Style;
public IntPtr WindowProcedure;
public int ClassAdditionalBytes;
public int WindowAdditionalBytes;
public IntPtr Instance;
public IntPtr Icon;
public IntPtr Cursor;
public IntPtr BackgroundBrush;
public char* ClassMenuResourceName;
public char* ClassName;
public IntPtr SmallIcon;
}
internal static unsafe WindowClass ConvertToManaged(WindowClassUnmanaged unmanaged)
{
return new()
{
WindowProcedure = Marshal.GetDelegateForFunctionPointer<Win32API.WindowProcedure>(unmanaged.WindowProcedure),
ClassMenuResourceName = MarshalHelpers.Win32WideCharArrToString(unmanaged.ClassMenuResourceName),
ClassName = MarshalHelpers.Win32WideCharArrToString(unmanaged.ClassName),
// (remainder omitted, just simple copies)
};
}
internal unsafe ref struct ManagedToUnmanagedIn
{
public static int BufferSize => sizeof(WindowClassUnmanaged);
private byte* UnmanagedBufferStruct;
private char* UnmanagedStrResourceName, UnmanagedStrClassName;
public void FromManaged(WindowClass managed, Span<byte> buffer)
{
IntPtr WindowProcedure = (managed.WindowProcedure == null) ? IntPtr.Zero : Marshal.GetFunctionPointerForDelegate(managed.WindowProcedure);
this.UnmanagedStrResourceName = (managed.ClassMenuResourceName == null) ? null : (char*)Marshal.StringToHGlobalUni(managed.ClassMenuResourceName);
this.UnmanagedStrClassName = (managed.ClassName == null) ? null : (char*)Marshal.StringToHGlobalUni(managed.ClassName);
WindowClassUnmanaged Result = new()
{
WindowProcedure = WindowProcedure,
ClassMenuResourceName = this.UnmanagedStrResourceName,
ClassName = this.UnmanagedStrClassName,
// (remainder omitted, just simple copies)
};
Span<byte> ResultByteView = MemoryMarshal.Cast<WindowClassUnmanaged, byte>(MemoryMarshal.CreateSpan(ref Result, 1));
Debug.Assert(buffer.Length >= ResultByteView.Length, "Target buffer isn't large enough to hold the struct data.");
ResultByteView.CopyTo(buffer);
this.UnmanagedBufferStruct = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(buffer));
}
public byte* ToUnmanaged() => this.UnmanagedBufferStruct;
public void Free()
{
if (this.UnmanagedStrResourceName != null)
{
Marshal.FreeHGlobal((nint)this.UnmanagedStrResourceName);
this.UnmanagedStrResourceName = null;
}
if (this.UnmanagedStrClassName != null)
{
Marshal.FreeHGlobal((nint)this.UnmanagedStrClassName);
this.UnmanagedStrClassName = null;
}
}
}
}
With this helper function to convert a Win32 LP(C)WSTR into a regular .NET string:
public static unsafe string? Win32WideCharArrToString(char* unmanagedArr)
{
if (unmanagedArr == null) { return null; }
int Length = 0;
while (*(unmanagedArr + Length) != 0x0000) { Length++; }
return Encoding.Unicode.GetString((byte*)unmanagedArr, Length * sizeof(char));
}
The nicer WindowClass struct is pretty much the same as before, except readonly, and with all elements being { get; init; }. The MarshalAs attributes on members are no longer required, as the custom marshaling handles everything.
Finally, the actual extern function now looks like this:
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvStdcall) })]
public static partial ushort RegisterClassEx([MarshalUsing(typeof(WindowClassMarshaler))] WindowClass classDefinition);
Note that this has been corrected. Previously I used the in keyword on the parameter, but this causes it to pass a pointer to the pointer to the struct data, which is an extra level of indirection that will cause the code to fail. Above is the updated version that works correctly.
I've tested and verified this works both in regular publish modes, as well as with AOT compilation, which was the reason for using LibraryImport in this case.
My bonus question still stands however, is there any benefit in specifying stdcall using UnmanagedCallConv?

.NET 6 IDispatch client implementation crash

.NET 6 has trouble calling its own IDispatch objects, if marshaled.
To reproduce:
if (Array.IndexOf(Environment.GetCommandLineArgs(), "/s")>=0) //Server
{
Thread t = new Thread(new ThreadStart(() =>
{
Hello h = new Hello();
new RunningObjectTable().Register("HelloTest", h);
Thread.Sleep(1000 * 3600);
}));
t.SetApartmentState(ApartmentState.MTA);
t.Start();
Thread.Sleep(1000 * 3600);
}
else //Client
{
object o = new RunningObjectTable().Get("HelloTest");
IHello h = o as IHello;
int f = h.Foo();
Console.WriteLine(h);
}
[ComImport]
[Guid("00020400-0000-0000-C000-000000000046")] //IID_IDispatch
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[TypeLibType(TypeLibTypeFlags.FDispatchable)]
public interface IHello
{
[MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.InternalCall)]
[DispId(1)]
public int Foo();
}
public class Hello: IHello
{
public int Foo()
{
Debug.WriteLine("Hello from server");
return 19;
}
}
Run the program with /s, that's the object server. Then run another copy to become the client.
The method invocation line in the client crashes with "Member not found" - HRESULT 0x80020003, DISP_E_MEMBERNOTFOUND. Normally it would mean a bogus DISPID, but where would the discrepancy possibly come from?
One minor infraction in that code is that the interface is decorated with the IID of IDispatch. With a custom IID, as COM prescribes, it doesn't even unmarshal as the dispinterface (the as IHello line returns null); internally, there's a QueryInterface call across processes with said custom IID, the dispinterface is not registered under HKCR\Interfaces, so the interprocess COM machinery doesn't know how to marshal it. At least that's my theory.
A similar piece of logic works fine if the server is a native (C++) one. If the same C# piece is recompiled against .NET framework 4.72, it doesn't even get that far, the o as IHello; line returns null.
RunningObjectTable is a helper class around the ROT. For completeness' sake, here:
internal class RunningObjectTable
{
#region API
[DllImport("ole32.dll")]
private static extern int CreateItemMoniker([MarshalAs(UnmanagedType.LPWStr)] string
lpszDelim, [MarshalAs(UnmanagedType.LPWStr)] string lpszItem,
out IMoniker ppmk);
[DllImport("ole32.dll")]
private static extern int GetRunningObjectTable(int reserved, out IRunningObjectTable prot);
#endregion
private IRunningObjectTable m_rot;
public RunningObjectTable()
{
GetRunningObjectTable(0, out m_rot);
}
private IMoniker CreateItemMoniker(string s)
{
IMoniker mon;
CreateItemMoniker("", s, out mon);
return mon;
}
public int Register(string ItemName, object o)
{
return m_rot.Register(0, o, CreateItemMoniker(ItemName));
}
public void Unregister(int ROTCookie)
{
m_rot.Revoke(ROTCookie);
}
public object Get(string ItemName)
{
object o;
m_rot.GetObject(CreateItemMoniker(ItemName), out o);
return o;
}
}
Can someone with access to .NET Core internals please tell me what's going on?
Here's what is going on. The .NET object supports two dispinterfaces - one corresponding to IHello, another corresponding to Hello itself, and the latter is treated as the default one. I've confirmed that by adding an extra method Bar() to Hello, and checking the type in a native C++ client:
IUnknownPtr pUnk;
ROTGet(L"HelloTest", &pUnk);
IDispatchPtr pDisp(pUnk);
LPOLESTR Name = (LPOLESTR)L"Bar";
DISPID dispid;
hr = pDisp->GetIDsOfNames(IID_NULL, &Name, 1, 0, &dispid);
And I got S_OK and a valid, if bogus, DISPID. I then requested a DISPID for Foo in the same way, and got another bogus DISPID.
I've found two ways to fix that.
One, tell the framework that it should not implicitly generate a dispinterface for Hello. Annotate the class with [ClassInterface(ClassInterfaceType.None)], and it won't. The explicit check for the DISPID of Foo confirms that. The unmarshaled dispinterface in the client corresponds to IHello, and the methods work as expected.
Another approach, give IHello an identity (an IID) and make it marshalable. If I slap a custom GUID on that interface, and add a key with that GUID to HKCR\Interfaces, providing ProxyStubClsid32 equals to {00020420-0000-0000-C000-000000000046} (that's PSDispatch, the proxy/stub CLSID for IDispatch), the fragment also works.
One other way to tell COM that an IID corresponds to a dispinterface and should be marshaled as one is via implementing IStdMarshalInfo. I didn't try this.
One more note: the Hello class has to be declared public for this to work. If it's not, for some reason the as IHello returns null.

COM interop and marshaling of VARIANT(VT_PTR)

We use a 3rd party COM object, one of which methods under certain conditions returns a VARIANT of VT_PTR type. That upsets the default .NET marshaler, which throws the following error:
Managed Debugging Assistant 'InvalidVariant' : 'An invalid VARIANT was
detected during a conversion from an unmanaged VARIANT to a managed
object. Passing invalid VARIANTs to the CLR can cause unexpected
exceptions, corruption or data loss.
Method signatures:
// (Unmanaged) IDL:
HRESULT getAttribute([in] BSTR strAttributeName, [retval, out] VARIANT* AttributeValue);
// C#:
[return: MarshalAs(UnmanagedType.Struct)]
object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);
Is there an elegant way to bypass such marshaler's behavior and obtain the underlying unmanaged pointer on the managed side?
What I've considered/tried so far:
A custom marshaler:
[return: MarshalAs(UnmanagedType.CustomMarshaler,
MarshalTypeRef = typeof(IntPtrMarshaler))]
object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);
I did implement IntPtrMarshaler, just to find the interop layer crashing the process even before any of my ICustomMarshaler methods gets called. Perhaps, the VARIANT* argument type is not compatible with custom marshalers.
Rewrite (or clone) the C# interface definition with getAttribute method redefined (like below) and do all the marshaling for output VARIANT manually:
void getAttribute(
[In, MarshalAs(UnmanagedType.BStr)],
string strAttributeName,
IntPtr result);
This doesn't seem nice (the interface itself has 30+ other methods). It'd also break existing, unrelated pieces of code which already make use of getAttribute without issues.
Obtain an unmanaged method address of getAttribute from vtable (using Marshal.GetComSlotForMethodInfo etc), then do the manual invocation and marshaling against my own custom delegate type (using Marshal.GetDelegateForFunctionPointer etc).
So far, I've taken this approach and it seem to work fine, but it feels as such an overkill for what should be a simple thing.
Am I missing some other feasible interop options for this scenario? Or, maybe there is a way to make CustomMarshaler work here?
What I would do is define a simple VARIANT structure like this:
[StructLayout(LayoutKind.Sequential)]
public struct VARIANT
{
public ushort vt;
public ushort r0;
public ushort r1;
public ushort r2;
public IntPtr ptr0;
public IntPtr ptr1;
}
And the interface like this;
[Guid("39c16a44-d28a-4153-a2f9-08d70daa0e22"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface MyInterface
{
VARIANT getAttributeAsVARIANT([MarshalAs(UnmanagedType.BStr)] string strAttributeName);
}
Then, add an extension method somewhere in a static class like this, so the caller can have the same coding experience using MyInterface:
public static object getAttribute(this MyInterface o, string strAttributeName)
{
return VariantSanitize(o.getAttributeAsVARIANT(strAttributeName));
}
private static object VariantSanitize(VARIANT variant)
{
const int VT_PTR = 26;
const int VT_I8 = 20;
if (variant.vt == VT_PTR)
{
variant.vt = VT_I8;
}
var ptr = Marshal.AllocCoTaskMem(Marshal.SizeOf<VARIANT>());
try
{
Marshal.StructureToPtr(variant, ptr, false);
return Marshal.GetObjectForNativeVariant(ptr);
}
finally
{
Marshal.FreeCoTaskMem(ptr);
}
}
This will do nothing for normal variants, but will just patch it for VT_PTR cases.
Note this only works if the caller and the callee are in the same COM apartement.
If they are not, you will get the DISP_E_BADVARTYPE error back because marshaling must be done, and by default, it will be done by the COM universal marshaler (OLEAUT) which only support Automation compatible data types (just like .NET).
In this case, theoratically, you could replace this marshaler by another one (at COM level, not at NET level), but that would mean to add some code on C++ side and possibly in the registry (proxy/stub, IMarshal, etc.).
For my own future reference, here's how I ended up doing it, using the 3rd option mentioned in the question:
[ComImport, Guid("75A67021-058A-4E2A-8686-52181AAF600A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInterface
{
[return: MarshalAs(UnmanagedType.Struct)]
object getAttribute([In, MarshalAs(UnmanagedType.BStr)] string strAttributeName);
}
private delegate int IInterface_getAttribute(
IntPtr pInterface,
[MarshalAs(UnmanagedType.BStr)] string name,
IntPtr result);
public static object getAttribute(this IInterface obj, string name)
{
var ifaceType = typeof(IInterface);
var ifaceMethodInfo = ((Func<string, object>)obj.getAttribute).Method;
var slot = Marshal.GetComSlotForMethodInfo(ifaceMethodInfo);
var ifacePtr = Marshal.GetComInterfaceForObject(obj, ifaceType);
try
{
var vtablePtr = Marshal.ReadIntPtr(ifacePtr);
var methodPtr = Marshal.ReadIntPtr(vtablePtr, IntPtr.Size * slot);
var methodWrapper = Marshal.GetDelegateForFunctionPointer<IInterface_getAttribute>(methodPtr);
var resultVar = new VariantClass();
var resultHandle = GCHandle.Alloc(resultVar, GCHandleType.Pinned);
try
{
var pResultVar = resultHandle.AddrOfPinnedObject();
VariantInit(pResultVar);
var hr = methodWrapper(ifacePtr, name, pResultVar);
if (hr < 0)
{
Marshal.ThrowExceptionForHR(hr);
}
if (resultVar.vt == VT_PTR)
{
return resultVar.ptr;
}
try
{
return Marshal.GetObjectForNativeVariant(pResultVar);
}
finally
{
VariantClear(pResultVar);
}
}
finally
{
resultHandle.Free();
}
}
finally
{
Marshal.Release(ifacePtr);
}
}

.Net COM object needs to be a STA Thread

I'm implementing a .NET COM Server for a 3rd party system which is the COM client (C++). I had a IDL provided which I used to generate the .dll with tlbimp. In the API its also recommended to implement the interfaces dual and Apartment. In this COM server I have to instantiate a WPF component, which currently fails because it has to be called from a STA Thread. I did a lot of googeling already on how to enforce my COM Server to be of Threading model Apartment. I changed the registry entry which didn't help and then I found following article on stackoverflow: How to make make a .NET COM object apartment-threaded?
I followed the instructions to implement ICustomQueryInterface but it fails; Here my IDL:
[
object,
uuid(ABB09FED-F7BE-4654-88BC-5F4D2941111A),
dual,
nonextensible,
helpstring("IDVRServer Interface"),
pointer_default(unique)
]
interface IDVRServer : IDispatch{
[id(1), helpstring("Creates a DVR Unit Connection based on connection information
such as IP address, user name...")]
HRESULT CreateConnection(
[in] DVRConnectionInfo* pConnInfo, [out, retval] IDVRUnitConnection** ppConn);
};
Here the implementation in C# (including the ICustomQueryInterface):
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)
public class DVRServer : IDVRServer, ICustomQueryInterface
{
private IntPtr _dvrPointer;
public DVRServer() {
NativeMethods.CoGetStdMarshalEx(this, NativeMethods.SMEXF_SERVER, out _dvrPointer);
}
public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv) {
if (iid == NativeMethods.IID_IMarshal_DVRServerDispatch)
{
res = 0;
res = Marshal.QueryInterface(_dvrPointer, ref NativeMethods.IID_IMarshal_DVRServerDispatch, out ppv);
string hex = res.ToString("X");
if (res != 0)
{
return CustomQueryInterfaceResult.Failed;
}
return CustomQueryInterfaceResult.Handled;
}
return CustomQueryInterfaceResult.Failed;
}
static class NativeMethods
{
public static Guid IID_IMarshal_DVRServerDispatch = new Guid("ABB09FED-F7BE-4654-88BC-5F4D2941111A");
public const UInt32 SMEXF_SERVER = 1;
[DllImport("ole32.dll", PreserveSig = true)]
public static extern void CoGetStdMarshalEx([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, UInt32 smexflags, out IntPtr ppUnkInner);
}
The result of Marshal.QueryInterface is always 0x80004002 E_NOINTERFACE. I tried a lot of different things already, also deriving from StandardOleMarshalObject and ServicedComponent but nothing worked. One question is also if the registry does count at all or if I can just ignore the ThreadingModel in there, I didn't really get that from Noseratio's post. Thanks in advance for any help on that topic.
I use VisualStudio 2012 and .Net Framework 4.

Exposing indexer like properties to COM

I have in existing COM-interface. I wan't to create a .net assembly that exposes a new interface as COM (with a new GUID), but the structure of the interface needs to be the same.
How can i create a .net class (C#) that exposes this interface?
[
odl,
uuid(1ED4C594-DDD7-402F-90DE-7F85D65560C4),
hidden,
oleautomation
]
interface _IFlashPhase : IUnknown {
[propget]
HRESULT _stdcall ComponentName(
[in] short i,
[out, retval] BSTR* pVal);
[propput]
HRESULT _stdcall ComponentName(
[in] short i,
[in] BSTR pVal);
[propget]
HRESULT _stdcall ComponentMolePercent(
[in] short i,
[out, retval] double* pVal);
[propput]
HRESULT _stdcall ComponentMolePercent(
[in] short i,
[in] double pVal);
[propget]
HRESULT _stdcall ComponentFugacity(
[in] short i,
[out, retval] double* pVal);
[propput]
HRESULT _stdcall ComponentFugacity(
[in] short i,
[in] double pVal);
};
Your IDL isn't valid, an interface that is attributed with [oleautomation] should derive from IDispatch, not IUnknown. I'll give the proper declarations and hint where you need to modify them to get yours.
You cannot declare indexed properties in C#, the C# team refuses to implement them. Version 4 has support for indexed properties that are declared in a COM type library but still doesn't allow declaring them yourself. The workaround is to use the VB.NET language, it has no qualms about it. Add a VB.NET class library project to your solution. Make it look similar to this:
Imports System.Runtime.InteropServices
Namespace Mumble
<ComVisible(True)> _
<Guid("2352FDD4-F7C9-443a-BC3F-3EE230EF6C1B")> _
<InterfaceType(ComInterfaceType.InterfaceIsDual)> _
Public Interface IExample
<DispId(0)> _
Property Indexer(ByVal index As Integer) As Integer
<DispId(1)> _
Property SomeProperty(ByVal index As Integer) As String
End Interface
End Namespace
Note the use of <DispId>, dispid 0 is special, it is the default property of an interface. This corresponds to the indexer in the C# language.
All you need VB.NET for is the declaration, you can still write the implementation of the interface in the C# language. Project + Add Reference, Projects tab and select the VB.NET project. Make it look similar to this:
using System;
using System.Runtime.InteropServices;
namespace Mumble {
[ComVisible(true)]
[Guid("8B72CE6C-511F-456e-B71B-ED3B3A09A03C")]
[ClassInterface(ClassInterfaceType.None)]
public class Implementation : ClassLibrary2.Mumble.IExample {
public int get_Indexer(int index) {
return index;
}
public void set_Indexer(int index, int Value) {
}
public string get_SomeProperty(int index) {
return index.ToString();
}
public void set_SomeProperty(int index, string Value) {
}
}
}
You need to run Tlbexp.exe on both the VB.NET and the C# assembly to generate the type libraries. The C# one with the implementation includes the VB.NET one.
To get the interface to derive from IUnknown instead of IDispatch, edit the interface declaration. Remove the DispId attributes and use ComInterfaceType.InterfaceIsUnknown.

Categories

Resources