4

A COM component is currently implemented in C++ and the next version must be implemented in C#. The component is called from C++ (not CLI) code. The major part of the not so small component is already ported to C#, but I have trouble to figure out how one specific method can be translated.

Interface definition

The following imports are used in the IDL:

import "oaidl.idl";
import "ocidl.idl";

The major part of the interface is already translated to the corresponding C# attributes, interfaces and classes. Most of it works without problems.

But one of the interface members is defined in IDL as

    [
        object,
        uuid(<guid>),
        helpstring("Help"),
        pointer_default(unique)
    ]
    interface IStuff : IUnknown
    {
...
        HRESULT GetShortText([in] int i, [in] BOOL b, [out, string] TCHAR shortText[10]);
...
    }

Usage

To use the interface, a local TCHAR[10] array is passed as the name of the array (therefore, as a TCHAR*). The COM server must put a short string in the TCHAR array.

Problem

I can't manage to call the method. Breakpoints set in the C# COM server in the GetShortText method are never hit. COM simply doensn't find my implementation in .NET.

How can this method that is called with a fixed size TCHAR* be correctly implemented in C#?

pvoosten
  • 3,247
  • 27
  • 43
  • What do you mean by a "short string"? Short is usually int16. A TCHAR is also referred to a WCHAR (W for wide) which is a unicode string. So I think the issue is going from a UTF-8 encoded string to a unicode string. In c# a string is a class where characters are either one or two bytes and a protected property indicating if a character is one or two bytes. So to convert from teh c# class string to a TCHAR you need to using encoding 1) byte[] bytes Encoding.Unicode.GetBytes(string) 2) string s = Encoding.Unicode.GetString(bytes) – jdweng Oct 20 '20 at 09:54
  • Its not completely clear to me if the C# code shall *call* this method or *implement* it. Also, TCHAR can be char or wchar_t depending on how you compile the C++ code. This needs to be fixed and known to the C# code as well. – Klaus Gütter Oct 20 '20 at 09:57
  • @jdweng with a *short* string I mean only a couple of characters (of type TCHAR in C++), not a series of shorts. The problem is how to marshal TCHAR* to .NET. I haven't tried byte[] in C# yet. Thanks for the suggestion. – pvoosten Oct 20 '20 at 10:06
  • @KlausGütter C# shall *implement* this. It is possible to let the C# implementation know whether char or wchar_t is used. Both are possible indeed. There is a unicode and a non-unicode variant of the C++ application that consumes the COM interface. – pvoosten Oct 20 '20 at 10:09
  • When *calling*, you can use `StringBuilder`, see [Default Marshaling for Strings](https://learn.microsoft.com/en-us/dotnet/framework/interop/default-marshaling-for-strings#fixed-length-string-buffers). Not sure however how to *implement* such an interface in C#. – Klaus Gütter Oct 20 '20 at 10:29
  • I tried `StringBuilder` and `byte[]` and didn't manage to mimic the IDL with any of them. – pvoosten Oct 20 '20 at 10:49
  • 1
    As a last resort, you might use `GetShortText(int i, [MarshalAs:UnmanagedType.Bool] bool b, IntPtr shortTextPtr)` and then use methods from the `Marshal` class to stuff the characters into the buffer. – Klaus Gütter Oct 20 '20 at 10:53
  • Without `MarshalAs` attribute for the IntPtr? – pvoosten Oct 20 '20 at 10:55
  • Well, that's unfortunate that the person that defined the interface didn't make it Automation compatible so there could be simple marshaling. Original interface should have been something like HRESULT GetShortText([in] int i, [in] VARIANT_BOOL b, [out,retval] BSTR* shortText); And it's crazy that it is defined as a TCHAR. A TCHAR can be a char or a WCHAR depending on how a project is compiled. – Joseph Willcoxson Oct 20 '20 at 14:55
  • The idl is uncommon. `TCHAR shortText[10]` won't even compile with midl, so you can't even build a tlb with this. I'm curious to how this convention is enforced across COM apartments (marshaling?). Just pass an IntPtr and make sure you don't overwrite your buffer "by convention". Or give us a complete small reproducing sample. – Simon Mourier Oct 20 '20 at 15:28

2 Answers2

2

I am not aware of any standard marshalling for this rather unusual construct. You might do the following, however:

void GetShortText(int i, [MarshalAs:UnmanagedType.Bool] bool b, IntPtr shortTextPtr)
{
    string s = "Test";
    byte[] buffer;
    if (UseUnicode)
        buffer = Encoding.Unicode.GetBytes(s + '\0');
    else
        buffer = Encoding.Default.GetBytes(s + '\0');
    Marshal.Copy(buffer, 0, shortTextPtr, buffer.Length);
}

Notes:

  • a range check on buffer.Length (<= 10 for ANSI, <= 20 for UNICODE) should be added
  • The + '\0' is for null-terminating the returned string
Klaus Gütter
  • 11,151
  • 6
  • 31
  • 36
  • This implementation doesn't get called by the C++ client application, so it is not interchangeable with a COM server defined by the given IDL. – pvoosten Oct 20 '20 at 12:50
  • Did you register your assembly using regasm? Does the client application reveive any error? Can you debug the client application? – Klaus Gütter Oct 20 '20 at 13:00
  • You said "COM server": so this is not an in-proc scenario? "An error is logged in the Application event log": which error? – Klaus Gütter Oct 20 '20 at 13:22
  • This is an in-proc scenario where a coclass instance is created with CoCreateInstance. The consuming part and the COM "server" (or what should I call it?) can't be debugged simultaneously. Application error with event id 1000 category 100. There are WER files, but they don't contain useful info. The error is that a COM interface member can't be found. I ran into this before. – pvoosten Oct 20 '20 at 15:34
  • "can't be debugged simultaneously" Why this? The more you tell about your environment the stranger it seems. – Klaus Gütter Oct 20 '20 at 15:55
1

This sort of interface can be implemented in .Net by specifying the parameter as an IntPtr and manually marshalling the string. A full demo can be found on Github.

Example Implementation:

[ComVisible(true), Guid("E559D616-4C46-4434-9DF7-E9D7C91F3BA5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IStuff
{
    void GetShortTest(int i, [MarshalAs(UnmanagedType.Bool)] bool b, IntPtr shortText);
}

[ComVisible(true), ProgId("InProcTargetTest.Class1"), Guid("BA5088D4-7F6A-4C76-983C-EC7F1BA51CAA"), ClassInterface(ClassInterfaceType.None)]
public class Class1 : IStuff
{
    public void GetShortTest(int i, bool b, IntPtr shortText)
    {
        var data = Encoding.Unicode.GetBytes("Hello");
        Marshal.Copy(data, 0, shortText, data.Length);
    }
}

Example caller:

if (FAILED(CoInitialize(nullptr))) {
    std::cout << "Failed to initialize COM\n";
}

IStuff *pStuff = nullptr;
CLSID clsClass1 = { 0 };
if (FAILED(CLSIDFromProgID(L"InProcTargetTest.Class1", &clsClass1))
    || FAILED(CoCreateInstance(clsClass1, nullptr, CLSCTX_INPROC_SERVER, IID_IStuff, (LPVOID*)&pStuff))) {
    std::cout << "Failed to create COM instance\n";
}

TCHAR test[10] = { 0 };
pStuff->GetShortTest(5, true, test);

std::cout << "From C#: " << test << "\n";

Example IDL:

import "unknwn.idl";

[
    odl,
    uuid(E559D616-4C46-4434-9DF7-E9D7C91F3BA5),
    version(1.0),
    pointer_default(unique),
    custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "InProcTargetTest.IStuff")

]
interface IStuff : IUnknown {
    HRESULT _stdcall GetShortTest(
        [in] long i,
        [in] BOOL b,
        [out, string] TCHAR shortText[10]);
};
Mitch
  • 21,223
  • 6
  • 63
  • 86
  • I thought you already had an IDL you were trying to meet? Why are you using the .Net TLB? (from a binary perspective on Win32 `LONG`, `LPVOID`, and `TCHAR[]` are all the same) – Mitch Apr 13 '21 at 20:58
  • That's right. This is a correct answer for the question (disregarding the string encoding). – pvoosten Apr 13 '21 at 21:02