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.
.