Accessing Delphi DLL throwing ocasional exception
Asked Answered
S

2

1

When I'm calling a Dll method it sometimes throws an exception, and sometimes doesn't.

I'm calling it like this:

public class DllTest
{

    [DllImport(@"MyDll.dll")]
    public extern static string MyMethod(string someStringParam);
}


class Program
{       

    static void Main(string[] args)
    {
        DllTest.MyMethod("SomeString");
    }
}

And the exception I get sometimes is this:

AccessViolationException

Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

Does anyone have any idea why I only get this exception sometimes? Why does it run smoothly sometimes?

Sandie answered 16/12, 2011 at 11:25 Comment(3)
What's the calling convention of the function in the DLL?Propitious
Please show the code for the Delphi methodTrunnion
Big chance declartion of the function in Delphi differs from its C# counterpartHiga
T
13

You clearly have a mismatch between the p/invoke code and the Delphi code. You have not shown the Delphi code but the C# code is enough to know what the Delphi code should look like.

Your DllImport attribute uses default values for calling convention and character set. This means that the calling convention is stdcall and the character set is ANSI. You have not specified any marshalling attributes so default marshalling must be used.

Therefore your Delphi code must look like this:

function MyMethod(someStringParam: PChar): PChar; stdcall;
begin
  Result := ??;
end;

And now here's the problem. The p/invoke marshaller treats a string return value in a very special way. It assumes that it is the responsibility of the p/invoke marshaller to deallocate the return value's memory. And it has to use the same allocator as the native code. The assumption made by the marshaller is that the shared COM allocator will be used.

So the rule is that the native code must allocate the memory with the COM allocator by calling CoTaskMemAlloc. My bet is that your code does not do that and that will certainly result in errors.

Here is an example of how you could make a native Delphi function that works with the C# signature in your code.

function MyMethod(someStringParam: PChar): PChar; stdcall;
var
  Size: Integer;
begin
  Size := SizeOf(Char)*(StrLen(someStringParam)+1);//+1 for zero-terminator
  Result := CoTaskMemAlloc(Size);
  Move(someStringParam^, Result^, Size);
end;

Whilst you could adopt this approach I recommend an alternative. Marshal all your strings as BSTR on the C# side and WideString on the Delphi side. These are matching types which are also allocated by the COM allocator. Both parties know exactly what to do with these types and will make your life easier.

Unfortunately, you cannot return a WideString from a Delphi function across an interop boundary because Delphi uses a different ABI for function return values. More details of this issue can be found in my question Why can a WideString not be used as a function return value for interop?

So to work around that, we can declare the return type from the Delphi code to be TBStr. Your code would then look like this:

C#

[DllImport(@"MyDll.dll")]
[return: MarshalAs(UnmanagedType.BStr)]
private static extern string MyMethod(
    [MarshalAs(UnmanagedType.BStr)]
    string someStringParam
);

Delphi

function MyMethod(someStringParam: WideString): TBStr; stdcall;
begin
  Result := SysAllocString(POleStr(someStringParam));
end;
Trunnion answered 16/12, 2011 at 11:40 Comment(5)
Your answer was awesome and cleared lots of things up. Just now I realized I posted the extern method as string, but it was void. But that's no problem because it made you take that in consideration and it just added to the answer. Still, would you mind giving me another example? How should I consume a Delphi function with a string parameters passed by ref (out)? And thanks a lot for the long clear and awesome answer, you've really helped me.Sandie
Do it with a BSTR/WideString. It's a var or out parameter on the Delphi side and a ref or out on the C# side. Really, BSTR/WideString is the way to make this easy.Trunnion
In Delphi 5 when I set the return type to WideString the dll crashes. The .Net application reports "External component has thrown an exception." Could this be an issue with Delphi 5? I've copied your example exactly.Syngamy
@Jamie The variant with WideString does not work. I learnt this after I wrote this answer. See my question here: #9350030 That question presents two workaroundsTrunnion
@JamieKitson The reviewers rejected your suggested edit. Thanks for trying, but the answer needed quite a bit more work to make it sound. I've now done that. I think the answer is now sound. The code at the bottom of the answer will work. And I've added a link to my question on the topic of WideString return values. I hope this satisfies your needs now.Trunnion
C
2

For me, marshalling of Delphi WideString to .Net string using UnmanagedType.BStr works very well in case of In and Out parameters. But it fails in case of functions returning strings. I have a Delphi functions -

function WS(val: WideString): WideString; stdcall;
begin
  result := val;
end;

procedure WS1(out result: widestring); stdcall;
begin
  result := 'ABCDE';
end;

and a correspondent .Net declarations -

[DllImport(@"my.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.BStr)]
static extern string WS(
  [MarshalAs(UnmanagedType.BStr)]
  string val
  );

[DllImport("my.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)]
static extern void WS1(
  [MarshalAs(UnmanagedType.BStr)]
  out string res);

calling to WS1() works just fine, while WS() raises an exception. And exception depends on what units are included in a Delphi project. If "SysUtils" or "Classes" are included - the .Net application raises a SEHException "External component has thrown an exception", if both units are excluded, the application shows a "Runtime error 203 at 009C43B4" error dialog and terminates its execution. BTW, usage of "ShareMem" unit doesn't change anything.

Cleland answered 24/1, 2012 at 17:15 Comment(1)
This isn't an answer. It is a restatement of this question: #9350030Trunnion

© 2022 - 2024 — McMap. All rights reserved.