3

i'm define in C# this interface for a COM-Server:

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("58C77969-0E7D-3778-9999-B7716E4E1111")]
public interface IMyInterface    
{
    string MyName { get; }
}

This interface is imported and implemented in a Delphi XE5 program.

The import looks like this:

IMyInterface = interface(IUnknown)
  ['{58C77969-0E7D-3778-9999-B7716E4E1111}']
  function Get_MyName (out pRetVal: WideString): HResult; stdcall;
end;

The implementation like this:

type
  TMyImpl = class(TInterfacedObject, IMyInterface)
  public
    function Get_MyName (out pRetVal: WideString): HResult; stdcall;    
 end;

 function TMyImpl.Get_MyName (out pRetVal: WideString): HResult;
 var
  s: string;
 begin
   s:=''; // empty!
   pRetVal:=s;
   result:=S_OK;
 end;

When i call that server from c# like this:

var server = new Server();
string s = server.MyName;

Then s is NULL and not an empty string as excepted.

How i can force that empty strings are transferred in COM as empty string and not replace by marshaling to NULL?

coding Bott
  • 4,287
  • 1
  • 27
  • 44

2 Answers2

3

Delphi implements empty strings as nil pointers (see System._NewUnicodeString). You can allocate an empty COM-compatible string manually:

function TMyImpl.Get_MyName(out pRetVal: WideString): HResult;
var
  BStr: TBstr;
begin
  BStr := SysAllocString('');
  if Assigned(BStr) then
  begin
    Pointer(pRetVal) := BStr;
    Result := S_OK;
  end
  else
    Result := E_FAIL;
end;

or you could create a helper function:

function EmptyWideString: WideString;
begin
  Pointer(Result) := SysAllocString('');
end;
Ondrej Kelle
  • 36,941
  • 2
  • 65
  • 128
  • 1
    You must not destroy the string. Ownership passes to the caller. Unless a copy is made. Perhaps it is. In which case I'm talking nonsense. In anycase it's surely easier to avoid WideString here. – David Heffernan Jan 08 '16 at 14:17
  • @DavidHeffernan Correct, thanks! Removed freeing the string. But `SysAllocString` can fail. – Ondrej Kelle Jan 08 '16 at 14:23
  • I don't see much point of using try/finally here. The function is contracted not to raise. It would seem cleaner to move `pRetVal := ''; Result := E_FAIL;` into an `else` statement and remove the try/finally. Ok, I see your edit, I think that's cleaner. To be honest, if SysAllocString fails, then we're hosed, but I do take your point. Personally I think that's something I'd be prepared to assume always works. But I recognise that other views are valid. – David Heffernan Jan 08 '16 at 14:25
  • Hi, i tested your solution. The conversion pRetVal := Bstr; will cause again a null pointer if the string is empty. Widestring as parameter typ seems to be a problem. but this i can't change, because interface is generated from c# tlb. – coding Bott Jan 08 '16 at 16:05
  • try typecasting: `Pointer(pRetVal) := BStr`; – Ondrej Kelle Jan 08 '16 at 16:15
  • That pointer cast did the trick, thank you. – coding Bott Jan 11 '16 at 09:57
2

Try this on the Delphi side:

IMyInterface = interface(IUnknown)
  ['{58C77969-0E7D-3778-9999-B7716E4E1111}']
  function Get_MyName (out pRetVal: BSTR): HResult; stdcall;
end;

function TMyImpl.Get_MyName (out pRetVal: BSTR): HResult;
begin
  pRetVal := SysAllocString('');
  Result := S_OK;
end;

If you wish to handle the case where SysAllocString fails then you would write it like this:

function TMyImpl.Get_MyName (out pRetVal: BSTR): HResult;
begin
  pRetVal := SysAllocString('');
  Result := IfThen(Assigned(pRetVal), S_OK, E_FAIL);
end;

Although personally I feel that it is reasonable to draw the line at check for errors on a call to SysAllocString('').

My guess is that Delphi marshals an empty WideString as a nil pointer rather than an empty BSTR. Which in my view is a defect.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Hi, your solutins using TBStr works. But the declarations are made in C# and the TLB is imported to Delphi with tlbimp.exe. Now i need an idea how i can force that the tlb import using tbstr instead of widestring without modify the import every time when its generated. – coding Bott Jan 08 '16 at 16:04
  • I think you need to post process the imported interface – David Heffernan Jan 08 '16 at 16:14
  • Using dotnet framework RegAsm.exe csharp.dll /tlb then with delphi's tlibimp.exe -P+ -Ha- -Hr- -Hs- -DImports csharp.tlb the result you can see in my question. this is part of an build script, which compiles that c# stuff first, followed by the import, then compile of delphi stuff. – coding Bott Jan 08 '16 at 16:16
  • Indeed. So post-process the imported interface. Or use a cast as TOndrej shows you. I think your question has been answered now and you are moving onto the specifics of what you do with the answer. That's fundamentally not the remit of this question. – David Heffernan Jan 08 '16 at 16:19