I did similar things before some years. A very pragmatic Approach (and maybe also a very dirty one).
Keep in mind I noted here what I remember...
The c# part
public class CSharpCom
{
private delegate void CPPCallBack([MarshalAs(UnmanagedType.BStr)] string text);
CPPCallBack _CPPCallBack= null;
public void SetCallBacks(IntPtr cppCallBack)
{
_CPPCallBack= (CPPCallBack)Marshal.GetDelegateForFunctionPointer(
cppCallBack,
typeof(CPPCallBack));
}
public void LogMsg(string msg)
{
try
{
_CPPCallBack?.Invoke(msg);
}
catch
{
}
}
}
The c++ part
static void __stdcall CPPCallBack(BSTR msg)
{
}
int wmain(int argc, char* argv[])
{
CoInitialize(0);
BSTR thing_to_send = ::SysAllocString(L"10 20");
BSTR returned_thing;
CSharpDll::_TheClassPtr obj(__uuidof(CSharpDll::TheClass));
obj->SetCallBacks((INT64)CPPCallBack);
HRESULT hResult = obj->GetTheThing(thing_to_send, &returned_thing);
if (hResult == S_OK) {
std::wcout << returned_thing << std::endl;
return 0;
}
return 1;
}
I hope it helps
[Edit]
code adjusted according to OP's feedback