Search code examples
c#.net-corepinvoke.net-assemblycorert

Call a managed C# library with P/invoke without Assembly.Load or similar


I've been around for days looking for this but all I find is how to call unmanaged libraries from C# using P/Invoke. I need to do it differently: I'm looking to use P/Invoke to call a managed assembly from another one (or use something else, avoiding to call Assembly.Load, Assembly.LoadFrom, etc.) basically due to a CoreRT/NativeAOT limitation (see here).

Basically, the idea of using CoreRT/NativeAOT is due to the native executable generation, which would improve a bit my app's security as common decompilers won't work with it (except IDA and a clever dev with ASM knowledge, but they're harder to come by). Considering that CoreRT/NativeAOT cannot (it can, but the .NET team just doesn't want to right now...) load external assemblies using any of the .NET Interop methods (Assembly.Load, Assembly.LoadFrom, etc.) but it can use DllImport, I'd like to call an external assembly that I don't really care much if it's decompiled or not without using any of those Assembly loading methods.

Yeah, I know I could write a wrapper or something with CoreRT itself to generate a native lib and call it with P/Invoke from the app, but in the case of Entity Framework which doesn't compile due to Reflection.Emit, it's not a possibility.

The ideal solution here would be to know how to call any .NET assembly (DLL) from another C# app/assembly WITHOUT using Assembly.Load/LoadFrom/LoadFromStream/etc. using other methods, be either P/Invoke (can it?) or other ones.


Solution

  • SOLUTION

    Thanks to @ChristianHeld for his suggestion to use .NET 5.0's Native Exports (plus also thanks to Aaron Robinson for his code. I got it working this way:

    .csproj:

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>net5.0</TargetFramework>
        ADD THIS -> <EnableDynamicLoading>true</EnableDynamicLoading>
        ADD THIS -> <DnneAddGeneratedBinaryToProject>true</DnneAddGeneratedBinaryToProject>
      </PropertyGroup>
    
      <ItemGroup>
        ADD THIS -> <PackageReference Include="DNNE" Version="1.*" />
      </ItemGroup>
    
    </Project>
    

    Library .NET Code (stripped):

    [UnmanagedCallersOnlyAttribute]
    public static void Init()
    {
        // ... Some code here...
    }
    

    Caller .NET Code (sample):

    class Program
    {
        const string LIBNAME = @"LibraryNE.dll";
        [DllImport(LIBNAME)] public static extern void Init();
    
        static void Main(string[] args)
        {
            Init();
        }
    }
    

    IMPORTANT: When running with VSCode (F5) it may say something like this:

    Failed to initialize context for config: C:\Project\Library.runtimeconfig.json. Error code: 0x80008092

    Disregard this, as it seems to happen because VSCode debugs the app with dotnet.exe, disregarding each assembly's runtimeconfig.json (somehow it throws LibHostInvalidArgs. No idea why).

    If you execute your app's .EXE directly, it will work (just used it with a CoreRT/NativeAOT-generated binary). The solution to this is to edit your launch.json file and change .dll with .exe in configurations -> program.

    Also, remember to copy the .NET dll as well. It seems that when you publish, only the native library (suffixed with NE) is copied, but it won't work by itself as it's just a wrapper. It needs the real .NET dll to be in the same folder as well.

    The debugging experience isn't so great, as when you debug the method it seems to use a different, non-existing source file so the line numbers when you step through don't match. It's debuggable, but you'll be doing so against ghost/offset lines.