Search code examples
c#.netinno-setuppascalscript

How to call .NET DLL from Inno Setup using DNNE?


I have previously successfully used Unmanaged Exports and DllExport to use .NET DLL files with Inno Setup.

However now I am trying to get it to work with DNNE.

I have the following C# code targeting x86

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <EnableDynamicLoading>true</EnableDynamicLoading>
    <Platforms>x86</Platforms>
    <RuntimeIdentifier>win-x86</RuntimeIdentifier>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="DNNE" Version="1.0.31" />
  </ItemGroup>

</Project>
using System.Runtime.InteropServices;

namespace DNNETest
{
    internal static class NativeMethods
    {
        [DllImport("User32.dll", EntryPoint = "MessageBox",
            CharSet = CharSet.Auto)]
        internal static extern int MsgBox(
            IntPtr hWnd, string lpText, string lpCaption, uint uType);
    }

    public class Class1
    {
        [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
        public static void Test()
        {
            _ = NativeMethods.MsgBox(IntPtr.Zero, "Hello from C#", ":)", 0);
            return;
        }
    }
}

I made a small console app to verify the exported code is working correctly:

using System.Runtime.InteropServices;

NE.Test();

public static class NE
{
    [DllImport("DNNETestNE", CallingConvention = CallingConvention.StdCall)]
    public extern static void Test();
}

Works OK!


Now I try to move it to Inno Setup:

[Files]
Source: Files\Dotnet\DNNETest.deps.json; Flags: dontcopy
Source: Files\Dotnet\DNNETest.dll; Flags: dontcopy
Source: Files\Dotnet\DNNETest.runtimeconfig.json; Flags: dontcopy
Source: Files\Dotnet\DNNETestNE.dll; Flags: dontcopy
procedure Test();
external 'Test@{tmp}\DNNETestNE.dll stdcall delayload';

procedure InitializeDotnet;
begin
  ExtractTemporaryFiles('{tmp}\DNNETest.deps.json');
  ExtractTemporaryFiles('{tmp}\DNNETest.dll');
  ExtractTemporaryFiles('{tmp}\DNNETest.runtimeconfig.json');
  ExtractTemporaryFiles('{tmp}\DNNETestNE.dll');
  Test();
end;

Will crash with Could not call proc

I also tried

external 'Test@{tmp}\DNNETestNE.dll,DNNETest.dll stdcall delayload loadwithalteredsearchpath';

Played around with combinations of AnyCPU, x86, x64 but to no avail

But same error

I am unsure what else I can try, since these steps were working ok with the other DllImport packages.


Solution

  • It does not work because

    The compiler also decorates C functions that use the __stdcall calling convention with an underscore (_) prefix and a suffix composed of the at sign (@) followed by the number of bytes (in decimal) in the argument list.

    Source: https://learn.microsoft.com/en-us/cpp/build/reference/exports?view=msvc-170

    The quick fix is to use cdecl instead of stdcall on both C# and Pascal definitions

    If you really want to use stdcall, keep reading...


    To fix it: Add this line to <PropertyGroup>

    <DnneWindowsExportsDef>$(MSBuildProjectDirectory)\DnneWindowsExports.def</DnneWindowsExportsDef>
    

    Add the following:

    EXPORTS
       Test=Test
    

    Replace Test with the function you want to export


    I made a small console app, which will generate this file: Just add a reference to your project and replace the classname in typeof to one with your exports.

    using DNNETest;
    using System.Text;
    
    var names = typeof(NativeExports).GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static).Select(m => m.Name).ToArray();
    
    var output = new StringBuilder();
    output.AppendLine("EXPORTS");
    
    foreach (var name in names)
    {
        output.AppendLine($"\t{name}={name}");
    }
    
    var result = output.ToString();
    Console.WriteLine(result);
    File.WriteAllText(@"SomeLocation\DnneWindowsExports.def", result);
    

    I made the following example to show it's working

    public static class NativeExports
    {
        [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
        public static void Test()
        {
            _ = NativeMethods.MsgBox(IntPtr.Zero, nameof(Test), "C#", 0);
        }
    
        [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
        public static void SendInt(int value)
        {
            _ = NativeMethods.MsgBox(IntPtr.Zero, $"{nameof(SendInt)}: {value}", "C#", 0);
        }
    
        [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
        public static void SendString(IntPtr value)
        {
            var message = Marshal.PtrToStringUni(value);
            _ = NativeMethods.MsgBox(IntPtr.Zero, $"{nameof(SendString)}: {message}", "C#", 0);
        }
    
        [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
        public static unsafe void ReturnString(IntPtr value, IntPtr* result)
        {
            var message = Marshal.PtrToStringUni(value);
    
            var returnString = new string(message.Reverse().ToArray());
            _ = NativeMethods.MsgBox(IntPtr.Zero, $"{nameof(ReturnString)}: {message} => {returnString}", "C#", 0);
    
            *result = Marshal.StringToBSTR(returnString);
        }
    
        [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
        public static int ReturnInt(int input)
        {
            return input;
        }
    
        public delegate bool ExpandConstantDelegate([MarshalAs(UnmanagedType.LPWStr)] string input, [MarshalAs(UnmanagedType.BStr)] out string output);
        [UnmanagedCallersOnly(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })]
        public static void CallExpandConstantCallback(IntPtr callbackPtr)
        {
            var ExpandConstant = Marshal.GetDelegateForFunctionPointer<ExpandConstantDelegate>(callbackPtr);
    
            var constant = "{tmp}";
            ExpandConstant(constant, out var result);
    
            _ = NativeMethods.MsgBox(IntPtr.Zero, $"{nameof(ExpandConstant)}({constant}) => {result}", "C#", 0);
        }
    }
    
    procedure Test();
    external 'Test@{tmp}\DNNETestNE.dll stdcall delayload';
    
    procedure SendInt(value: Integer);
    external 'SendInt@{tmp}\DNNETestNE.dll stdcall delayload';
    
    procedure SendString(value: string);
    external 'SendString@{tmp}\DNNETestNE.dll stdcall delayload';
    
    procedure ReturnString(value: string; out outValue: WideString);
    external 'ReturnString@{tmp}\DNNETestNE.dll stdcall delayload';
    
    function ReturnInt(value: Integer) : Integer;
    external 'ReturnInt@{tmp}\DNNETestNE.dll stdcall delayload';
    
    procedure ExpandConstantWrapper(const toExpandString: string; out expandedString: WideString);
    begin
      expandedString := ExpandConstant(toExpandString);
    end;
    
    procedure CallExpandConstantCallback(callback: Longword);
    external 'CallExpandConstantCallback@{tmp}\DNNETestNE.dll stdcall delayload';
    
    procedure InitializeDotnet;
    var
      outString: WideString;
    begin
      ExtractTemporaryFiles('{tmp}\DNNETest*');
      Test();
      SendInt(1234);
      SendString('Hello World');
      ReturnString('ReverseMe!', outString);
      MessageBox(outString, 0);
      MessageBox(IntToStr(ReturnInt(4321)), 0);
      CallExpandConstantCallback(CreateCallback(@ExpandConstantWrapper));
    end;