AaronRobinsonMSFT / DNNE

Prototype native exports for a .NET Assembly.
MIT License
404 stars 41 forks source link

Native Exports for .NET

DNNE

Prototype for a .NET managed assembly to expose a native export.

This work is inspired by work in the Xamarin, CoreRT, and DllExport projects.

Requirements

Minimum

DNNE NuPkg Requirements

Windows:

macOS:

Linux:

Exporting a managed function

The Sample directory contains an example C# project consuming DNNE. There is also a native example, written in C, for consumption options.

Native code customization

The mapping of .NET types to their native representation is addressed by the concept of blittability. This approach however limits what can be expressed by the managed type signature when being called from an unmanaged context. For example, there is no way for DNNE to know how it should describe the following C struct in C# without being enriched with knowledge of how to construct marshallable types.

struct some_data
{
    char* str;
    union
    {
        short s;
        double d;
    } data;
};

The following attributes can be used to enable the above scenario. They are automatically generated into projects referencing DNNE, because DNNE provides no assembly to reference. If your build system or IDE does not support source generators (e.g., you're using a version older than Visual Studio 2022, or .NET Framework with packages.config), you will have to define these types yourself:

namespace DNNE
{
    /// <summary>
    /// Provide C code to be defined early in the generated C header file.
    /// </summary>
    /// <remarks>
    /// This attribute is respected on an exported method declaration or on a parameter for the method.
    /// The following header files will be included prior to the code being defined.
    ///   - stddef.h
    ///   - stdint.h
    ///   - dnne.h
    /// </remarks>
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false)]
    internal sealed class C99DeclCodeAttribute : System.Attribute
    {
        public C99DeclCodeAttribute(string code) { }
    }

    /// <summary>
    /// Define the C type to be used.
    /// </summary>
    /// <remarks>
    /// The level of indirection should be included in the supplied string.
    /// </remarks>
    [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)]
    internal sealed class C99TypeAttribute : System.Attribute
    {
        public C99TypeAttribute(string code) { }
    }
}

The above attributes can be used to manually define the native type mapping to be used in the export definition. For example:

public unsafe static class NativeExports
{
    public struct Data
    {
        public int a;
        public int b;
        public int c;
    }

    [UnmanagedCallersOnly]
    [DNNE.C99DeclCode("struct T{int a; int b; int c;};")]
    public static int ReturnDataCMember([DNNE.C99Type("struct T")] Data d)
    {
        return d.c;
    }

    [UnmanagedCallersOnly]
    public static int ReturnRefDataCMember([DNNE.C99Type("struct T*")] Data* d)
    {
        return d->c;
    }
}

In addition to providing declaration code directly, users can also supply #include directives for application specific headers. The DnneAdditionalIncludeDirectories MSBuild property can be used to supply search paths in these cases. Consider the following use of the DNNE.C99DeclCode attribute.

[DNNE.C99DeclCode("#include <fancyapp.h>")]

Generating a native binary using the DNNE NuPkg

1) The DNNE NuPkg is published on NuGet.org, but can also be built locally.

* Build the DNNE NuPkg locally by building [`create_package.proj`](./src/create_package.proj).

    `> dotnet build create_package.proj`

1) Add the NuPkg to the managed project that will be exporting functions.

* See [`DNNE.props`](./src/msbuild/DNNE.props) for the MSBuild properties used to configure the DNNE process. For example, the [`Sample.csproj`](./sample/Sample.csproj) has some DNNE properties set.

* Visual Studio has a [NuGet package manager](https://learn.microsoft.com/nuget/quickstart/install-and-use-a-package-in-visual-studio) that can be used to add DNNE to a particular project. Alternatively, the following XML snippet can be manually added to the managed project.

    ```xml
    <ItemGroup>
      <PackageReference Include="DNNE" Version="2.*" />
    </ItemGroup>
    ```

* If the NuPkg is built locally, remember to update the project's `nuget.config` to point at the local disk location of the recently built DNNE NuPkg.

1) Set the <EnableDynamicLoading>true</EnableDynamicLoading> property in the managed project containing the methods to export. This will produce a .runtimeconfig.json that is needed to activate the runtime when calling an export.

1) Define at least one managed function to export. See the Exporting a managed function section.

1) Build the managed project to generate the native binary. The native binary will have a NE suffix, this is configurable, and the system extension for dynamic/shared native libraries (that is, .dll, .so, .dylib).

1) Deploy the native binary, managed assembly and associated *.json files for consumption from a native process.

Generate manually

1) Run the dnne-gen tool on the managed assembly.

1) Use the generated source from dnne-gen along with the DNNE platform source to compile a native binary with the desired native exports. See the Native API section for build details.

1) Deploy the native binary, managed assembly and associated *.json files for consumption from a native process.

Experimental attribute

There are scenarios where updating UnmanagedCallersOnlyAttribute may take time. In order to enable independent development and experimentation, the DNNE.ExportAttribute is also respected. Like other DNNE attributes, this type is also automatically generated into projects referencing the DNNE package. This type can be modified to suit one's needs (by tweaking the generated source in dnne-analyzers) and dnne-gen updated as needed to respect those changes at source gen time.

namespace DNNE
{
    [AttributeUsage(AttributeTargets.Method, Inherited = false)]
    internal sealed class ExportAttribute : Attribute
    {
        public ExportAttribute() { }
        public string EntryPoint { get; set; }
    }
}

The calling convention of the export will be the default for the .NET runtime on that platform. See the description of CallingConvention.Winapi.

Using DNNE.ExportAttribute to export a method requires a Delegate of the appropriate type and name to be at the same scope as the export. The naming convention is <METHODNAME>Delegate. For example:

public class Exports
{
    public delegate int MyExportDelegate(int a);

    [DNNE.Export(EntryPoint = "FancyName")]
    public static int MyExport(int a)
    {
        return a;
    }
}

Native API

The native API is defined in src/platform/dnne.h.

The DNNE_ASSEMBLY_NAME must be set during compilation to indicate the name of the managed assembly to load. The assembly name should not include the extension. For example, if the managed assembly on disk is called ClassLib.dll, the expected assembly name is ClassLib.

The following defines are set based on the target OS platform:

The generated source will need to be linked against the nethost library as either a static lib (libnethost.[lib|a]) or dynamic/shared library (nethost.lib). If the latter linking is performed, the nethost.[dll|so|dylib] will need to be deployed with the export binary or be on the path at run time.

The set_failure_callback() function can be used prior to calling an export to set a callback in the event runtime load or export discovery fails.

Failure to load the runtime or find an export results in the native library calling abort(). See FAQs for how this can be overridden.

The preload_runtime() or try_preload_runtime() functions can be used to preload the runtime. This may be desirable prior to calling an export to avoid the cost of loading the runtime during the first export dispatch.

.NET Framework support

.NET Framework support is limited to the Windows platform. This limitation is in place because .NET Framework only runs on the Windows platform.

DNNE has support for targeting .NET Framework v4.x TFMs—there is no support for v2.0 or v3.5. DNNE respects multi-targeting using the TargetFrameworks MSBuild property. For any .NET Framework v4.x TFM, DNNE will produce a native binary that will activate .NET Framework.

In non-.NET Framework scenarios, .deps.json files are generated during compilation that help, at run-time, to find assemblies. This is different than .NET Framework, which has a more complicated application model. One area where this difference can be particularly confusing is during initial load and activation of a managed assembly. Tools like fuslogvw.exe can help to understand loading failures in .NET Framework.

Due to how .NET Framework is being activated in DNNE, the managed DLL typically needs to be located next to the running EXE rather than the native DLL produced by DNNE. Alternatively, the EXE loading the DNNE generated DLL can define a .config file that defines probing paths.

FAQs

Additional References

dotnet repo

nethost example