Calling a Delphi method in a dll from c#
Asked Answered
V

3

5

I am trying to call a method in a Delphi DLL with the following signature:

 function SMap4Ovr(const OverFileName       : ShortString    ;
                    const Aclay              : Integer        ;
                    const Acarbon            : Double         ;
                    out   errstr             : ShortString): WordBool;

I am using the following import in C#:

        [DllImport("SMap.dll", CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Ansi)]
    public static extern bool SMap4Ovr(
        string OverFileName,
        int Aclay,
        double Acarbon,
        out string errstr
        );

But I am getting a AccessViolationException.

I seem to be able to call into a couple of simpler methods in the DLL that have string parameters but not ints or doubles.

I have also tried with the CallingConvention = CallingConvention.Cdecl but this gives me the same error.

Valiancy answered 17/5, 2013 at 4:29 Comment(0)
C
13

When writing interop code it is critical that both sides of the interface match in every way. Here are the main issues that you must make agree on both sides:

  1. Calling conventions.
  2. Parameters lists.
  3. Parameter types and semantics.

The first observation is that your calling conventions do not match. You have register on the Delphi side and stdcall on the C# side. The Delphi register convention is private to Delphi and so you should use stdcall.

Secondly, your string parameter types do not match. The Delphi shortstring is a data type that became legacy when Delphi 2 was released and should be considered a relic from the previous century. It was never a valid interop type, and there's nothing in the p/invoke framework that can be used to match it. Whilst you could attempt to do the marshalling by hand, this is a lot of work that is simply not needed when there are simple solutions available. You should try to forget all about shortstring.

You need to use a string type that both sides of the interface can work with. You could use null-terminated C strings, but a better and simpler choice is the COM BSTR which is WideString in Delphi.

So, the final result is as follows.

Delphi

function SMap4Ovr(
    OverFileName: WideString;
    Aclay: Integer;
    Acarbon: Double;
    out errstr: WideString
): WordBool; stdcall;

C#

[DllImport("SMap.dll")]
public static extern bool SMap4Ovr(
    [MarshalAs(UnmanagedType.BStr)] 
    string OverFileName,
    int Aclay,
    double Acarbon,
    [MarshalAs(UnmanagedType.BStr)] 
    out string errstr
);

I did not bother specifying the calling convention on the DllImport since the default is stdcall. If you prefer you can be explicit about this.

Be careful when using WideString that you don't attempt to use it as a return value. Because Delphi uses non-standard semantics for return values, you can only use simple types that fit into a register as return values.

Cresting answered 17/5, 2013 at 6:55 Comment(3)
Thanks David that was a great help.Valiancy
I solved my problem with this post. +1 MarshalAs did the trick!Skink
dear sir, David, can you please have a look at the following link and see whether you can help me out? csharpforums.net/threads/call-delphi-dll-from-c.9721Horehound
R
0

Default calling convention in Delphi is register, not stdcall. It seems calling conventions details show us that Microsoft fastcall is not the same as Borland fastcall (register)

And C# string type differs from Delphi ShortString (it contains internally one byte length + string body)

Reyreyes answered 17/5, 2013 at 5:26 Comment(1)
fastcall is indeed different from register, and in any case not supported by the pinvoke marshallerCresting
M
0

OK,

It's an old question, but I think times have changed, and this might help other developers who like to use Delphi for the "heavy lifting" and .net because..." ""

Here is a white paper showing how to call Delphi from c++ and c#, it is also old but I guess it has good information in it

I think what often gets forgotten is the fact that you need to deal with the memory, who allocates it, who releases it. And if Delphi allocates it and passes it to c# ory PHP or any other language, you need to "call back" and inform Delphi that the memory can be released.

DELPHI In you library, you need to export the functions; if you do a cross-platform library, you will use:

library Native;
{$R *.res}
uses
  MemUtils in 'MemUtils.pas',
exports

{$IFDEF MACOS}
  ,_freeMemory // in MemUtils.pas
  ,_encryptBytes // in SecureData.pas
  ,_decryptBytes;
{$ELSE}
  ,_freeMemory // in MemUtils.pas
  ,EncryptBytes // in SecureData.pas
  ,DecryptBytes;
{$ENDIF} 
begin    
end.

MacOS needs _ for the functions because... its a mac thing related to ... that's how they want it ;-) See documentation

Now, the implementation, Let's do free memory first:

{$IFDEF MACOS}
procedure _freeMemory(var ptr: Pointer); stdcall;
{$ELSE}
procedure FreeMemory(var ptr: Pointer); stdcall;
{$ENDIF}

begin
  try
    if Assigned(ptr) then
    begin
      FreeMem(ptr);
      Log('Memory released', etInfo);
      ptr := nil;
    end;
  except
    on E: Exception do
    begin
      // Handle exceptions if necessary, for example, logging or setting error states
      Log('%s: %s. Stack trace: %s', [E.ClassName, E.Message, E.StackTrace],
        etCritical);

      ptr := nil;
    end;
  end;
end;

.NET/ C#

First, let's do some "plumbing". You will need the following directory structure for each platform you would like to use in your deployment if you use a nugget package and share your code with your other packages or the world and create:

.\runtime \osx-x64 \native \libNative.dylib \win-64 \native \Native.dll

Android is different because... why not. You need to put android binaries in: .\libs \arm64-v8 libNative.so \armeabi-v7a libNative.so

Now, to make it part of NuGet, you need to add the following lines in your .csprod file:

<ItemGroup>
    <!-- Add binary resources to nuget for Mac and Windows platforms-->
    <None Include=".\runtimes\win-x64\native\Native.dll" Pack="true" PackagePath="runtimes/win-x64/native" />
    <None Include=".\runtimes\ios-x64\native\libNative.dylib" Pack="true" PackagePath="runtimes/ios-x64/native" />
    <None Include=".\runtimes\ios-arm64\native\liNative.dylib" Pack="true" PackagePath="runtimes/ios-arm64/native" />
    <None Include=".\libs\X86_64\libNative.so" Pack="true" PackagePath="libs/X86_64" />
    <!-- macOS -->
    <None Include=".\runtimes\osx-x64\native\libNative.dylib" Pack="true" PackagePath="runtimes/osx-x64/native" />
    <None Include=".\runtimes\osx-arm64\native\libNative.dylib" Pack="true" PackagePath="runtimes/osx-arm64/native" />

</ItemGroup>

<ItemGroup>
    <!-- Add Android resources to nuget need a specific directory -->
    <None Include="libs\arm64-v8a\libNative64.so" Pack="true" PackagePath="libs/arm64-v8a" />
    <None Include="libs\armeabi-v7a\libNative32.so" Pack="true" PackagePath="libs/armeabi-v7a" />
</ItemGroup>

You also need to/ target the frameworks you support with the Delphi binaries in your .csproj file like so:

<PropertyGroup>
    <TargetFrameworks>
        netstandard2.0;
        netstandard2.1;

        net6.0;net7.0;net8.0;

        net8.0-windows;
        net8.0-android;
        net8.0-ios;
        net8.0-maccatalyst;
        net8.0-macos;
    </TargetFrameworks>
    <LangVersion>latest</LangVersion>
<PropertyGroup>

In the .cspro file add the following line that will tell the build system what file needs to be copied based on what platform you are targeting:

<ItemGroup>
    <None Include="build\Wrapper.targets" Pack="true" PackagePath="build\" />
</ItemGroup>

I have placed the file in a directory called Build, but you can use any name you prefer.

The Wrapper.targets file should look something like this:

Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <!-- Example for Windows x64 -->
    <Target Name="CopyNativeBinaries" AfterTargets="Build" Condition="'$(TargetFramework)' == 'net8.0-windows'">
        <Copy SourceFiles="$(MSBuildThisFileDirectory)..\runtimes\win-x64\native\Native.dll"
              DestinationFolder="$(OutputPath)\" />
    </Target>
    <!-- macOS x64 -->
    <Target Name="CopyNativeBinariesMacOSX64" AfterTargets="Build" Condition="'$(TargetFramework)' == 'net8.0-macos'">
        <Copy SourceFiles="$(MSBuildThisFileDirectory)..\runtimes\osx-x64\native\libNative.dylib"
              DestinationFolder="$(OutputPath)\" />
    </Target>

    <!-- macOS ARM64 -->
    <Target Name="CopyNativeBinariesMacOSARM64" AfterTargets="Build" Condition="'$(TargetFramework)' == 'net8.0-macos'">
        <Copy SourceFiles="$(MSBuildThisFileDirectory)..\runtimes\osx-arm64\native\libNative.dylib"
              DestinationFolder="$(OutputPath)\" />
    </Target>

    <!-- iOS ARM64 -->
    <Target Name="CopyNativeBinariesIosARM64" AfterTargets="Build" Condition="'$(TargetFramework)' == 'net8.0-ios'">
        <Copy SourceFiles="$(MSBuildThisFileDirectory)..\runtimes\ios-arm64\native\libNative.dylib"
              DestinationFolder="$(OutputPath)\" />
    </Target>

    <!-- iOS x64 (Simulator) -->
    <Target Name="CopyNativeBinariesIosX64" AfterTargets="Build" Condition="'$(TargetFramework)' == 'net8.0-ios'">
        <Copy SourceFiles="$(MSBuildThisFileDirectory)..\runtimes\ios-x64\native\libNative.dylib"
              DestinationFolder="$(OutputPath)\" />
    </Target>
    <!-- Linux x64 I remane the file from .so to .xso otherwise the android macro will complain-->
    <Target Name="CopyNativeBinariesLinuxX64" AfterTargets="Build" Condition="'$(TargetFramework)' == 'net8.0' and '$(RuntimeIdentifier)' == 'linux-x64'">
        <Copy SourceFiles="$(MSBuildThisFileDirectory)..\runtimes\linux-x64\native\libNative.xso"
              DestinationFolder="$(OutputPath)\" />
    </Target>
</Project>

I included logging, but that's optional. in C# you'd use the following constants as that makes life easier when dealing with cross-platform implementation

[DebuggerBrowsable(DebuggerBrowsableState.Never)]
#if WINDOWS        
    const string _fileName = "Native.dll";
    
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    const string _freeMemory = "FreeMemory.dll";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    const string _encryptBytes = "EncryptBytes";

#elif MACOS
    const string _fileName = "libNative.dylib";   
    
    [DebuggerBrowsable(DebuggerBrowsableState.Never)] 
    const string _freeMemory ="_freeMemory";  

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    const string _encryptBytes = "_encryptBytes";
#elif ANDROID
    const string _fileName = "libNative.so";
    
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    const string _freeMemory = "FreeMemory.dll";

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    const string _encryptBytes = "EncryptBytes";        
#else
#else
    const string _fileName = "Native.dll";//less than .net 6
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    const string _freeMemory = "FreeMemory";
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    const string _encryptBytes = "EncryptBytes";

#endif

OK, now let's implement the P/Invoke in the class where you defined the constants

#if NET7_0_OR_GREATER
                                            
[LibraryImport(_fileName, EntryPoint = _encryptBytes, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool EncryptBytes(byte[] clearBytes,
                                        int protection,
                                        [MarshalAs(UnmanagedType.LPWStr)] string password,
                                        out IntPtr encryptedBytes,
                                        out int outputSize);

[LibraryImport(_fileName, EntryPoint = _decryptBytes, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DecryptBytes(byte[] encryptedBytes,
                                        int protection,
                                        [MarshalAs(UnmanagedType.LPWStr)] string password,
                                        out IntPtr clearBytes,
                                        out int outputSize);

[LibraryImport(_fileName, EntryPoint = _freeMemory)]
[UnmanagedCallConv(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
static partial void FreeMemory(ref IntPtr ptr);

#else

[DllImport(_fileName, EntryPoint = _encryptBytes, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EncryptBytes(
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] byte[] clearBytes,
    int protection,
    [MarshalAs(UnmanagedType.LPWStr)] string password,
    out IntPtr encryptedBytes,
    out int outputSize);

[DllImport(_fileName, EntryPoint = _decryptBytes, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DecryptBytes(
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] byte[] encryptedBytes,
    int protection,
    [MarshalAs(UnmanagedType.LPWStr)] string password,
    out IntPtr clearBytes,
    out int outputSize);

[DllImport(_fileName, EntryPoint = "FreeMemory", CallingConvention = CallingConvention.StdCall)]
static extern void FreeMemory(ref IntPtr ptr);
#endif

Now, for the implementation:

public bool TryDecrypt(in byte[] secureBytes, in SecureMemoryProtection protection, in string password, [NotNullWhen(true)] out byte[]? clearBytes)
{
    clearBytes = null;
    int use = 0;
    if (protection.HasFlag(SecureMemoryProtection.Machine))
    {
        use |= 1;
    }
    if (protection.HasFlag(SecureMemoryProtection.UserProfile))
    {
        use |= 2;
    }


    //calling delphi method
    if (DecryptBytes(secureBytes, use, password, out IntPtr clearPtr, out int outputSite))
    {
        try
        {
            clearBytes = new byte[outputSite];
            Marshal.Copy(clearPtr, clearBytes, 0, outputSite);
        }
        finally
        {
            //free the memory
            FreeMemory(ref clearPtr);
            clearPtr = IntPtr.Zero;
        }
    }
    return secureBytes is not null;
}

public bool TryEncrypt(in byte[] clearBytes, in SecureMemoryProtection protection, in string password, [NotNullWhen(true)] out byte[]? secureBytes)
{
    secureBytes = null;
    int use = 0;
    if (protection.HasFlag(SecureMemoryProtection.Machine))
    {
        use |= 1;
    }
    if (protection.HasFlag(SecureMemoryProtection.UserProfile))
    {
        use |= 2;
    }


    //calling delphi method
    if (EncryptBytes(clearBytes, use, password, out var encryptedPtr, out int encryptedSize))
    {
        try
        {
            //copy the pointer to the wide string
            secureBytes = new byte[encryptedSize];
            Marshal.Copy(encryptedPtr, secureBytes, 0, encryptedSize);
        }
        finally
        {
            //free the memory
            FreeMemory(ref encryptedPtr);
            encryptedPtr = IntPtr.Zero;
        }
    }
    return secureBytes is not null;
}

Some of you may have noticed that I free memory using a var pointer. I do this because my Delphi code sets the pointer to null, which maps to IntPtr.Zero. If releasing the memory would have failed, the pointer will still have an address - an invalid address, as the library likely crashed. However, this provides a way to test if memory addressing worked.

In my code, I make use of PWideChar instead of PChar. This is because PWideChar can handle Unicode characters, whereas PChar cannot. Additionally, I pass the size of the buffer back to the calling function instead of copying the entire buffer. This helps avoid buffer overflow or any other malware-related attacks that could potentially exploit buffer overflows. By returning only the required size of the buffer, I ensure that the buffer is not being accessed beyond its allocated size.

When you are Marchaling data one is marchaling delpi types to a "neutral type definition". I like to look at: Delphi to C++ types mapping

Wrapping it up: I like Rudy's Delphi Corner, he gives some useful insides. .

Moony answered 6/4, 2024 at 15:20 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.