I have created a minimal ActiveX Library project with Delphi 10.3 in 32-bit mode.
The COM server exposes a class Test which has 3 methods: EchoInt, EchoDouble and EchoString.
Each method writes the input value of the given type to the console and finally returns the unmodified input value.
Now I have created a minimal C# COM client with .NET 6.0 in 32-bit mode.
The client calls each method of the COM server and writes the return values to the console.
The console output of the C# client:
[int] Delphi receives: 1234
[int] C# receives: 1234
[double] Delphi receives: 1.23400000000000E+0000
[double] C# receives: NaN
[string] Delphi receives: hello
The values which Delphi receives are correct for all 3 value types.
However, the return value is only correct for type int.
For double it returns NAN instead of 1.234.
For string the return value marshalling seems to fail completely as the C# application immediately exits with return code != 0 without an exception.
I have tried 64-bit interop as well, which fails with different values.
I have also tried an out-of-process COM Server, which also yields the same faulty values.
How to reproduce? (ready-to-clone: https://github.com/timmi-on-rails/com-server)
Delphi COM Server
Create a Delphi ActiveX Library Project with the following relevant files.
ComServer.ridl
// ************************************************************************ //
// WARNING
// -------
// This file is generated by the Type Library importer or Type Libary Editor.
// Barring syntax errors, the Editor will parse modifications made to the file.
// However, when applying changes via the Editor this file will be regenerated
// and comments or formatting changes will be lost.
// ************************************************************************ //
// File generated on 30.01.2023 10:26:54 (- $Rev: 12980 $, 8627968).
[
uuid(92C99953-EA24-487D-A728-956B1338C264),
version(1.0)
]
library ComServer
{
importlib("stdole2.tlb");
interface ITest;
coclass Test;
[
uuid(C293FB27-B8BF-451C-90DE-7CE3C138B0E7),
helpstring("Interface for Test Object"),
oleautomation
]
interface ITest: IUnknown
{
[id(0x00000065)]
long _stdcall EchoInt([in] long i);
[id(0x00000066)]
double _stdcall EchoDouble([in] double d);
[id(0x00000067)]
LPWSTR _stdcall EchoString([in] LPWSTR s);
};
[
uuid(B44B1D0D-6824-41E3-A185-A97AB53C59BA),
helpstring("Test")
]
coclass Test
{
[default] interface ITest;
};
};
Test.pas:
unit Test;
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
Windows, ActiveX, Classes, ComObj, ComServer_TLB, StdVcl;
type
TTest = class(TTypedComObject, ITest)
protected
function EchoDouble(d: Double): Double; stdcall;
function EchoInt(i: Integer): Integer; stdcall;
function EchoString(s: PWideChar): PWideChar; stdcall;
end;
implementation
uses ComServ;
function TTest.EchoDouble(d: Double): Double;
begin
writeln('[double] Delphi receives: ', d);
Result := d;
end;
function TTest.EchoInt(i: Integer): Integer;
begin
writeln('[int] Delphi receives: ', i);
Result := i;
end;
function TTest.EchoString(s: PWideChar): PWideChar;
begin
writeln('[string] Delphi receives: ', s);
Result := s;
end;
initialization
TTypedComObjectFactory.Create(ComServer, TTest, Class_Test,
ciMultiInstance, tmApartment);
end.
Register COM Server
Open a command line with elevated rights and execute regsvr32 ComServer.dll to register the ActiveX COM library.
C# COM Client
Create a .NET 6 C# console project with the following relevant files.
ComClient.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Platforms>x86</Platforms>
</PropertyGroup>
<ItemGroup>
<COMReference Include="ComServer">
<WrapperTool>tlbimp</WrapperTool>
<VersionMinor>0</VersionMinor>
<VersionMajor>1</VersionMajor>
<Guid>92c99953-ea24-487d-a728-956b1338c264</Guid>
<Lcid>0</Lcid>
<Isolated>false</Isolated>
<EmbedInteropTypes>true</EmbedInteropTypes>
</COMReference>
</ItemGroup>
</Project>
Program.cs:
using System;
using ComServer;
var test = new Test();
Console.WriteLine("[int] C# receives: " + test.EchoInt(1234));
Console.WriteLine("[double] C# receives: " + test.EchoDouble(1.234));
Console.WriteLine("[string] C# receives: " + test.EchoString("hello"));
Console.WriteLine("done");
Since COM is a mature and established technology, I wonder, what am I doing wrong?
Related
I am following the example in How to call a managed DLL from native Visual C++ code in Visual Studio.NET or in Visual Studio 2005 to call a .NET DLL from native C++ code. The code for C# looks like this:
public class StringExample: IStringExample
{
public string returnString(string input)
{
return input;
}
}
I followed the example's steps to build, register the COM assembly, and export the type library (.tlb) from the C# code.
In C++ code, I am trying to use code similar to the following:
#import "..\StringExample\bin\Debug\StringExample.tlb" raw_interfaces_only
using namespace StringExample;
void abc()
{
// Initialize COM.
HRESULT hr = CoInitialize(NULL);
// Create the interface pointer.
IStringExample ptr(__uuidof(StringExample));
BSTR bstrInput = L"hello world";
BSTR bstrOutput =L"";
ptr->returnString(bstrInput, &bstrOutput);
TCHAR* szOutput = (TCHAR *)_bstr_t(bstrOutput);
// Uninitialize COM.
CoUninitialize();
}
However, bstrOutput is empty. Moreover, I need to convert the bstrOutput to TCHAR* to pass it to a different api.
Is there an error in the variable initialization? Is there a different way to pass string variables between .NET and C++?
First of all: There is existing Code using CreateDispatch. The maintainer doesn't want to change the code for compatibility/convenience reasons (except for using a new TLB/GUID).
So I have to create COM object which works with this restrictions. Preferabbly in C# (but C++ is also fine).
Problem is: I have absolutely no experience with COM.
That's how far I got: I created a COM object in C#, registered it and got a tlb. I checked the Registry, there is an entry: HKEY_CLASSES_ROOT\Wow6432Node\CLSID{36E6BC94-308C-4952-84E6-109041990EF7}
Seems fine. Next step: creating a test program (C++). I created a C++ console project with MFC enabled, imported the tlb. Then I added the following lines to the main:
CInterface01 server;
COleException* pe = new COleException;
LPTSTR m = new TCHAR[255];
CoInitialize(NULL);
server.CreateDispatch(L"{36E6BC94-308C-4952-84E6-109041990EF7}", pe);
pe->GetErrorMessage(m, 255);
Somehow the CreateDispatch didn't work. In the Exception it reads: "Class not registered"(what?! it's in the registry). Even worse: It crashes the Visual Studio when I'm running the same program again.
It feels like the solution is near, but I have no idea whats going wrong.
You need code like this:
//interface wrapper method implementations for
#import "YouTlbModule.tlb" no_namespace
//function
CoInitializeEx ( NULL, COINIT_MULTITHREADED);
IYouTlbModulePtr ptrYouTlbModule;
HRESULT hResult = ptrYouTlbModule.CreateInstance(OLESTR("Your.Component.Name"));
//test hResult
//Others function call
hResult = ptrYouTlbModule.Other(122, L"AAA");
//Call
int ret = ptrYouTlbInput.GetErrorMessage(m, 255);
Function calls from Java to C# through JNI-C++/CLI are failing when the C# COM is not registered using regasm with the codebase option. I've built a sample following the instructions in P2: Calling C# from Java with some changes.
Numero uno: C#
Change the C# dll into a COM by creating an interface, IRunner, and making the library assembly COM-visible.
namespace RunnerCOM
{
public interface IRunner
{
String ping();
}
public class Runner:IRunner
{
static void Main(string[] args)
{
}
public Runner() { }
public String ping()
{
return "Alive (C#)";
}
}
}
Numero due: Java
No changes made to the Java section.
Numero tre: C++
This part was changed to create a new instance of the RunnerCOM.Runner class and use that result. Here is a good tutorial on how to call managed code from unmanaged code: http://support.microsoft.com/kb/828736
#include "stdafx.h"
#include "Runner.h"
#pragma once
#using <mscorlib.dll>
#import "RunnerCOM.tlb"
JNIEXPORT jstring JNICALL Java_Runner_ping(JNIEnv *env, jobject obj){
RunnerCOM::IRunnerPtr t = RunnerCOM::IRunnerPtr("RunnerCOM.Runner");
BSTR ping = t->ping();
_bstr_t temp(ping, true);
char cap[128];
for(unsigned int i=0;i<temp.length();i++){
cap[i] = (char)ping[i];
}
return env->NewStringUTF(cap);
}
Now to my questions,
The code above fails with a _com_error exception, Class not registered (0x80040154) unless the codebase option is enabled during regsitration of RunnerCOM.dll, with regasm.exe. Why is this? If the code is not ran from JNI, I tested it as an exe, it works fine. The RunnerCOM.dll is simply found in the working directory.
Type casting _bstr_t temp to char* fails. For example, char *out = (char*) temp; Similar to the issue above, it works fine when it's built and executed as an exe but crashes the JVM when it's a JNI call.
By the way this is what I used to run it as an executable:
int main(){
RunnerCOM::IRunnerPtr t = RunnerCOM::IRunnerPtr("RunnerCOM.Runner");
BSTR ping = t->ping();
_bstr_t temp(ping, false);
printf(temp);
return 0;
}
Codebase creates a Codebase entry in the registry. The Codebase entry specifies the file path for an assembly that is not installed in the global assembly cache, so when you specify the codebase, the system will find the DLL based on the path. If not, it will try to locate the dll in the GAC and current working directory. In JNI, I think the current working directory is not the folder where the DLL is. You can use process explorer to find what is the current working directory, also, you can use process monitor to find out which directories the exe is looking into to find the dll.
The code converting _bstr_t to char*, the char* string cap is not ended with '\0', I think this might cause problem in JNI. Uses the _bstr_t operator (char *), you can obtain a null terminated string from the _bstr_t object. Please check the msdn example for details.
You mentioned C++/CLI, C++/Cli and COM warpper are two different ways to interop with C# code. If you're using C++/CLI as a bridge, you doesn't need to register C# DLL as COM, please see this: Calling .Net Dlls from Java code without using regasm.
If you're using COM, you should call CoInitialize() to init COM first in your code.
I want to use a dll that made by Delphi. It has this function :
function CryptStr(str, Key : AnsiString; DecryptStr : boolean) : AnsiString; stdcall;
I copied the Dll in /bin/debug and in application root. my code is :
[DllImport("Crypt2.dll", EntryPoint = "CryptStr", CallingConvention = CallingConvention.StdCall)]
static extern string CryptStr( string str, string Key, bool DecryptStr);
public string g = "";
private void Form1_Load(object sender, EventArgs e)
{
g=CryptStr("999", "999999", true);
MessageBox.Show(g);
}
I have some problem :
1. even I delete Dll from those path application doesn't throw not found exception
2. when application run in g=CryptStr("999", "999999", true); it finishes execution and show the form without running Messagebox line.
I tried to use Marshal but above errors remain.
You cannot expect to call that function from a programming environment other than Delphi. That's because it uses Delphi native strings which are not valid for interop. Even if you call from Delphi you need to use the same version of Delphi as was used to compile the DLL, and the ShareMem unit so that the memory manager can be shared. That function is not even well designed for interop between two Delphi modules.
You need to change the DLL function's signature. For example you could use:
procedure CryptStr(
str: PAnsiChar;
Key: PAnsiChar;
DecryptStr: boolean;
output: PAnsiChar;
); stdcall;
In C# you would declare this like so:
[DllImport("Crypt2.dll")]
static extern void CryptStr(
string str,
string Key,
bool DecryptStr,
StringBuilder output
);
This change requires the caller to allocate the buffer that is passed to the function. If you want to find examples of doing that, search for examples calling the Win32 API GetWindowText.
If you were using UTF-16 text instead of 8 bit ANSI, you could use COM BSTR which is allocated on the shared COM heap, but I suspect that option is not available to you.
As for your program not showing any errors, I refer you to these posts:
http://blog.paulbetts.org/index.php/2010/07/20/the-case-of-the-disappearing-onload-exception-user-mode-callback-exceptions-in-x64/
http://blog.adamjcooper.com/2011/05/why-is-my-exception-being-swallowed-in.html
Silent failures in C#, seemingly unhandled exceptions that does not crash the program
I Have a third party DLL write in Delphi "a.dll" (without source).
And this DLL has one method with this signature.
function GetAny(pFileName: String): String;
I can't do a interop call from c# because 'String type' has private access in delphi.
So a build another DLL in delphi to wrapper that call.
Delphi.
function GetAny(pFileName: String): String; external 'a.dll'
function GetWrapper(url : PChar) : PChar; stdcall;
begin
Result := PChar(GetAny(url)); // I need avoid this String allocation, is throwing a exception.
end;
C#.
[DllImport("wrapper.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
public static extern IntPtr GetWrapper(String url);
Inside "GetWrapper" i make a call to external "GetAny", the result is OK (in delphi i can debug), but before i get this result back in c# side, it's throwing a Exception.
IntPtr test = GetWrapper("a");
String result = Marshal.PtrToStringAnsi(test);
Your wrapper DLL also cannot call GetAny because string is a managed Delphi type that cannot be passed across module boundaries.
The problem is that the return value of GetAny is allocated in one module and deallocated in a different module. It is allocated in the DLL that implements GetAny, and deallocated in the DLL that calls GetAny. Since the two DLLs use different memory managers, you end up trying to deallocate memory that was allocated on a different heap.
If the DLL that implements GetAny can be persuaded to share a memory manager then you could solve that problem readily.
I do question the facts that you present though. As it stands, unless the DLL is designed to be used with ShareMem, that function can never be called safely.
If you were prepared to leak the memory you could try this:
Delphi
function GetAny(pFileName: string): PChar; external 'a.dll'
procedure GetWrapper(url: PChar; out value: WideString); stdcall;
var
P: PChar;
begin
P := GetAny(url);
if Assigned(P) then
Value := P
else
Value := '';
end;
C#
[DllImport("wrapper.dll"]
public static extern void GetWrapper(
string url,
[MarshalAs(UnmanagedType.BStr)]
out string value
);
I've downloaded your code...
The solution could be like this:
Create a wrapper procedure in Delphi with "cdecl" declaration with 2 parameters of PChar type
the first one is IN parameter
the second one is OUT parameter
Original Delphi function:
function GetAny(pFileName: String): String; external 'a.dll';
Delphi – DLL with wrapped function:
procedure GetWrapper (url: PChar; var urlNew: PChar) cdecl;
var str: string;
begin
urlStr = string(url);
urNewStr := GetAny(urlStr);
urlNew := PChar(urNewStr);
end;
exports
GetWrapper;
begin
end.
In Visual Studio:
Make the project x32 bit (not a x64 as it was in your sample)
Import DLL as Cdecl
[DllImport("wrapper.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
Marshaling
public static extern void GetWrapper ([MarshalAs(UnmanagedType.LPStr)]string url, [MarshalAs(UnmanagedType.LPStr)] out string urlNew);
Calling in C#:
string fileName;// = #"wertwertwertwertwer";
GetWrapper("2.jpg", out fileName);
Console.WriteLine(fileName);
In my environment it worked. (Delphi 5 and VS2012).
Hopefully it will work for you also.