Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a7d7c90
Add Rust as an alternative output language for DNNE
AaronRobinsonMSFT Feb 6, 2026
cc36213
Add Rust Cargo crate generation and ImportingProcess.Rust test
AaronRobinsonMSFT Feb 6, 2026
8999ba0
Update README to include Rust toolchain and examples for native exports
AaronRobinsonMSFT Feb 6, 2026
c8658c2
Apply suggestions from code review
AaronRobinsonMSFT Feb 7, 2026
4a95259
Enhance Rust crate generation by updating cleanup rules and adding cr…
AaronRobinsonMSFT Feb 7, 2026
b282bad
Apply suggestions from code review
AaronRobinsonMSFT Feb 7, 2026
d732288
Apply suggestions from code review
AaronRobinsonMSFT Feb 7, 2026
e3f4dec
Refactor project files to use variables for target framework and vers…
AaronRobinsonMSFT Feb 7, 2026
b5af00a
Build test.proj in CI
AaronRobinsonMSFT Feb 7, 2026
354d5b8
Fix linux path
AaronRobinsonMSFT Feb 7, 2026
a3c55bf
Fix Windows build
AaronRobinsonMSFT Feb 7, 2026
ca7a0fa
Update build configuration to conditionally disable package building …
AaronRobinsonMSFT Feb 8, 2026
6c3f841
Cargo release build.
AaronRobinsonMSFT Feb 8, 2026
3bc3204
Update abort handling in Rust platform module to use standard abort f…
AaronRobinsonMSFT Feb 8, 2026
3f1a1c2
Update README to clarify sample directory structure and Rust example …
AaronRobinsonMSFT Feb 8, 2026
c3253aa
Refactor various files
AaronRobinsonMSFT Feb 8, 2026
6830f9b
Review feedback
AaronRobinsonMSFT Feb 8, 2026
559d4cf
Incorrect argument to setup rust toolchain
AaronRobinsonMSFT Feb 8, 2026
6e09886
Remove Rust targeting on Windows.
AaronRobinsonMSFT Feb 8, 2026
6eed37c
Disable .NET 4.7.2 builds when using Rust.
AaronRobinsonMSFT Feb 8, 2026
b71a8a9
Build break.
AaronRobinsonMSFT Feb 8, 2026
48a3487
Update Rust export paths and add new input files for incremental builds
AaronRobinsonMSFT Feb 8, 2026
56a47db
Add safe identifier handling for Rust keywords in RustEmitter
AaronRobinsonMSFT Feb 8, 2026
9a878a9
Apply suggestions from code review
AaronRobinsonMSFT Feb 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copilot Instructions for DNNE

DNNE (Dotnet Native Exports) enables .NET managed assemblies to expose native exports consumable by native code. It generates C or Rust source from assemblies marked with `[UnmanagedCallersOnly]` and compiles them into platform-specific native binaries (`.dll`, `.so`, `.dylib`).

## Build & Test

```bash
# Build the NuGet package (includes all components)
dotnet build src/create_package.proj

# Run all unit tests
dotnet test test/DNNE.UnitTests

# Run a single test by name
dotnet test test/DNNE.UnitTests --filter "FullyQualifiedName~TestMethodName"

# Test with alternative compilers (useful for cross-compiler validation)
dotnet test test/DNNE.UnitTests -p:BuildWithGCC=true
dotnet test test/DNNE.UnitTests -p:BuildAsCPPWithMSVC=true
dotnet test test/DNNE.UnitTests -p:BuildWithClangPP=true
```

The test framework is **xUnit**. Tests target `net8.0` (and `net472` on Windows).

## Architecture

The project has a pipeline architecture where each component feeds into the next:

1. **dnne-analyzers** (`src/dnne-analyzers/`) — Roslyn source generator that emits the DNNE attribute types into consuming projects at compile time. Targets `netstandard2.0` and requires Roslyn 4.0+. Generated attributes include:
- `ExportAttribute`, `C99DeclCodeAttribute`, `C99TypeAttribute` — for C99 output
- `RustDeclCodeAttribute`, `RustTypeAttribute` — for Rust output

2. **dnne-gen** (`src/dnne-gen/`) — CLI tool (.NET 8.0) that reads a compiled managed assembly via reflection, finds methods marked with `[UnmanagedCallersOnly]` or `[DNNE.Export]`, and generates native source code with the corresponding export signatures. Supports two output languages selected via `-l`:
- `c99` (default) — generates C99 source with `DNNE_API` macros, C preprocessor platform guards, and lazy-init function pointer wrappers.
- `rust` — generates Rust source with `pub unsafe fn` wrappers intended for Rust callers (no `extern "C"` / `#[no_mangle]`), `AtomicPtr` lazy initialization, `#[cfg(target_os)]` platform guards, and `core::ffi` types.

The `Generator` class uses language-specific type providers (`C99TypeProvider` / `RustTypeProvider`) and emitters (`EmitC99` / `EmitRust`). Attribute detection is unified through `TryGetLanguageTypeAttributeValue` and `TryGetLanguageDeclCodeAttributeValue`, which select C99 or Rust attributes based on the output language. Exports with non-primitive value types that lack a type override are skipped in Rust mode.

3. **MSBuild integration** (`src/msbuild/`) — `DNNE.props` defines configurable properties; `DNNE.targets` wires up the build pipeline (run dnne-gen, then invoke the native compiler). `DNNE.BuildTasks/` contains custom MSBuild tasks with platform-specific compilation logic in `Windows.cs`, `Linux.cs`, and `macOS.cs`.

4. **Platform layer** (`src/platform/`) — Native source that bootstraps the .NET runtime via `nethost` and dispatches calls to managed exports.
- `platform.c` / `dnne.h` — C implementation with platform-conditional code (`#ifdef DNNE_WINDOWS` etc.) for library loading, path resolution, error state preservation, and thread-safe runtime initialization via spinlock.
- `platform_v4.cpp` — .NET Framework v4.x activation (C++ only).
- `platform.rs` — Rust implementation targeting .NET (Core) only. Uses `std::sync::Mutex` for thread-safe initialization, platform-specific `sys` modules for Unix/Windows, and a UTF-8 public API (`*const u8`) with internal wide-string conversion on Windows. Expects a crate-root constant `DNNE_ASSEMBLY_NAME` (for example, generated into `lib.rs` by `DNNE.targets`).

5. **dnne-pkg** (`src/dnne-pkg/`) — Orchestrates NuGet package creation, bundling analyzers, gen tool, build tasks, and platform source into the published package.

## Key Conventions

- Exported methods must be `public static` and marked with `[UnmanagedCallersOnly]` (preferred) or `[DNNE.Export]` (experimental). The enclosing class's accessibility doesn't matter.
- When using `[DNNE.Export]`, a companion `Delegate` type named `<MethodName>Delegate` must exist at the same scope.
- Custom native type mappings use language-specific attributes:
- **C99**: `[DNNE.C99Type("...")]` on parameters/return values and `[DNNE.C99DeclCode("...")]` on methods for struct definitions or `#include` directives.
- **Rust**: `[DNNE.RustType("...")]` on parameters/return values and `[DNNE.RustDeclCode("...")]` on methods for type definitions or `use` statements.
- The generated native binary is named `<AssemblyName>NE` by default (suffix controlled by `DnneNativeBinarySuffix` MSBuild property).
- MSBuild properties controlling behavior are defined in `src/msbuild/DNNE.props` — this is the reference for all DNNE configuration options.
- C# language version varies by component: `C# 11.0` for analyzers, `C# 9.0` for build tasks, default for other projects.
- The DNNE attribute types are source-generated into consuming projects — DNNE provides no assembly reference.
9 changes: 9 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ jobs:
run: |
dotnet clean test/DNNE.UnitTests -c ${{ matrix.flavor }}
dotnet test test/DNNE.UnitTests -c ${{ matrix.flavor }} -p:BuildWithGPP=true
- name: Build test.proj
run: |
dotnet build test/test.proj -c ${{ matrix.flavor }} -p:BuildPackage=false
- name: Upload Build Logs
if: failure()
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -71,6 +74,9 @@ jobs:
run: |
dotnet clean test\DNNE.UnitTests -c ${{ matrix.flavor }}
dotnet test test\DNNE.UnitTests -c ${{ matrix.flavor }}
- name: Build test.proj
run: |
dotnet build test\test.proj -c ${{ matrix.flavor }} -p:BuildPackage=false
- name: Upload Build Logs
if: failure()
uses: actions/upload-artifact@v4
Expand Down Expand Up @@ -98,6 +104,9 @@ jobs:
run: |
dotnet clean test/DNNE.UnitTests -c ${{ matrix.flavor }}
dotnet test test/DNNE.UnitTests -c ${{ matrix.flavor }} -p:BuildWithClangPP=true
- name: Build test.proj
run: |
dotnet build test/test.proj -c ${{ matrix.flavor }} -p:BuildPackage=false
- name: Upload Build Logs
if: failure()
uses: actions/upload-artifact@v4
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

**/bin/
**/obj/
**/target/
**/*.user
**/launchSettings.json

Expand Down
9 changes: 9 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<DnneTargetFramework>net8.0</DnneTargetFramework>
<DnneVersion>2.0.8</DnneVersion>

<RepoRoot>$(MSBuildThisFileDirectory)/</RepoRoot>
<SrcRoot>$(RepoRoot)src/</SrcRoot>
</PropertyGroup>
</Project>
119 changes: 114 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This work is inspired by work in the [Xamarin][xamarin_embed_link], [CoreRT][cor

* [.NET 8.0](https://dotnet.microsoft.com/download) or greater.
* [C99](https://en.cppreference.com/w/c/language/history) compatible compiler.
* [Rust](https://www.rust-lang.org/tools/install) toolchain (optional, for Rust output).

### DNNE NuPkg Requirements

Expand All @@ -28,10 +29,12 @@ This work is inspired by work in the [Xamarin][xamarin_embed_link], [CoreRT][cor
**macOS:**
* [clang](https://clang.llvm.org/) compiler on the path.
* Current platform and environment paths dictate native compilation support.
* For Rust output: `cargo` and `rustc` on the path.

**Linux:**
* [clang](https://clang.llvm.org/) compiler on the path.
* Current platform and environment paths dictate native compilation support.
* For Rust output: `cargo` and `rustc` on the path.

<a name="exporting"></a>

Expand Down Expand Up @@ -65,7 +68,7 @@ This work is inspired by work in the [Xamarin][xamarin_embed_link], [CoreRT][cor

- The manner in which native exports are exposed is largely a function of the compiler being used. On the Windows platform an option exists to provide a [`.def`](https://docs.microsoft.com/cpp/build/reference/exports) file that permits customization of native exports. Users can provide a path to a `.def` file using the [`DnneWindowsExportsDef`](./src/msbuild/DNNE.props) MSBuild property. Note that if a `.def` file is provided no user functions will be exported other than those defined in the `.def` file.

The [`Sample`](./sample) directory contains an example C# project consuming DNNE. There is also a [native example](./sample/native/main.c), written in C, for consumption options.
The [`Sample`](./sample) directory contains an example C# project consuming DNNE and a sub-directory consuming the export via C. There is also a [Rust example](./test/ImportingProcess.Rust), for consumption options.

### Native code customization

Expand Down Expand Up @@ -151,6 +154,44 @@ In addition to providing declaration code directly, users can also supply `#incl
[DNNE.C99DeclCode("#include <fancyapp.h>")]
```

### Rust native code customization

When targeting Rust output (`DnneLanguage=rust`), equivalent attributes are available for Rust type mappings. These are also automatically generated into projects referencing DNNE:

```CSharp
namespace DNNE
{
/// <summary>
/// Provide Rust code to be defined in the generated Rust source file.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false)]
internal sealed class RustDeclCodeAttribute : System.Attribute
{
public RustDeclCodeAttribute(string code) { }
}

/// <summary>
/// Define the Rust type to be used.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue, Inherited = false)]
internal sealed class RustTypeAttribute : System.Attribute
{
public RustTypeAttribute(string code) { }
}
}
```

For example:

```CSharp
[UnmanagedCallersOnly]
[DNNE.RustDeclCode("#[repr(C)] pub struct Data { pub a: i32, pub b: i32, pub c: i32 }")]
public static int ReturnDataMember([DNNE.RustType("Data")] Data d)
{
return d.c;
}
```

## Generating a native binary using the DNNE NuPkg

1) The DNNE NuPkg is published on [NuGet.org](https://www.nuget.org/packages/DNNE), but can also be built locally.
Expand Down Expand Up @@ -188,6 +229,56 @@ In addition to providing declaration code directly, users can also supply `#incl
* Although not technically needed, the exports header and import library (Windows only) can be deployed with the native binary to make consumption easier.
* Set the `DnneAddGeneratedBinaryToProject` MSBuild property to `true` in the managed project if it is desired to have the generated native binary flow with project references. Recall that the generated native binary is platform and architecture specific.

### Generating a Rust crate

DNNE can generate a [Cargo](https://doc.rust-lang.org/cargo/) crate instead of a compiled native binary. This allows Rust applications to consume .NET exports idiomatically as a crate dependency.

1) Set the `DnneLanguage` MSBuild property to `rust`:

```xml
<PropertyGroup>
<DnneLanguage>rust</DnneLanguage>
</PropertyGroup>
```

1) Build the managed project. Instead of compiling a native binary, DNNE generates a complete Cargo crate in the output directory under `dnne-rust-crate/`. The crate contains:
* `Cargo.toml` &mdash; package manifest with the assembly version and nethost link directives.
* `build.rs` &mdash; build script that configures library search paths and platform cfg flags.
* `lib.rs` &mdash; crate root that re-exports the `platform` and `exports` modules.
* `platform.rs` &mdash; Rust runtime hosting layer (equivalent of `platform.c`).
* `<AssemblyName>.g.rs` &mdash; generated export wrappers.

**Note** The generated `build.rs` will contain an absolute path on the build machine. The crate is only valid on the machine on which it was generated.

1) In the consuming Rust project, add the generated crate as a path dependency in `Cargo.toml`:

```toml
[dependencies]
# Update the path to match your build configuration and output directory.
myassembly-ne = { path = "../path/to/bin/Debug/net8.0/dnne-rust-crate" }
```

1) Use the exports from Rust:

```rust
use myassembly_ne::platform;
use myassembly_ne::exports;

fn main() {
unsafe {
let result = platform::try_preload_runtime();
assert!(result.is_ok());

let value = exports::MyExport(27);
println!("Result: {}", value);
}
}
```

1) Build and run with `cargo build` / `cargo run`. The managed assembly and its `*.runtimeconfig.json` must be located next to the consuming binary at run time.

See the [`ImportingProcess.Rust`](./test/ImportingProcess.Rust) project for a complete example.

### Generate manually

1) Run the [`dnne-gen`](./src/dnne-gen) tool on the managed assembly.
Expand Down Expand Up @@ -231,6 +322,8 @@ public class Exports

## Native API

### C99

The native API is defined in [`src/platform/dnne.h`](./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`.
Expand All @@ -249,11 +342,26 @@ Failure to load the runtime or find an export results in the native library call

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.

### Rust

When targeting Rust output, the native API is provided by the `platform` module in the generated crate. See [`src/platform/platform.rs`](./src/platform/platform.rs).

The `platform` module exposes the following public API:

* `set_failure_callback(callback: Option<fn(FailureType, i32)>)` &mdash; Set a callback for runtime load or export discovery failures. Unlike the C99 API, the callback uses a safe `fn` pointer wrapped in `Option`.
* `preload_runtime()` &mdash; Preload the .NET runtime. Calls `abort()` on failure.
* `try_preload_runtime() -> Result<(), i32>` &mdash; Preload the .NET runtime. Returns `Ok(())` on success or `Err(hresult)` on failure.
* `get_callable_managed_function(...)` / `get_fast_callable_managed_function(...)` &mdash; Resolve managed method function pointers. Used internally by the generated export wrappers.

The `FailureType` enum uses `#[repr(i32)]` with variants `LoadRuntime` and `LoadExport`.

Generated export functions are `pub unsafe fn`.

<a name="netfx"></a>

## .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.
.NET Framework support is limited to the Windows platform and C99. This limitation is in place because .NET Framework only runs on the Windows platform.

DNNE has support for targeting .NET Framework v4.x TFMs&mdash;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.

Expand All @@ -267,15 +375,16 @@ Due to how .NET Framework is being activated in DNNE, the managed DLL typically
* I am not using one of the supported compilers and hitting an issue of missing `intptr_t` type, what can I do?
* The [C99 specification](https://en.cppreference.com/w/c/types/integer) indicates several types like `intptr_t` and `uintptr_t` are **optional**. It is recommended to override the computed type using `DNNE.C99TypeAttribute`. For example, `[DNNE.C99Type("void*")]` can be used to override an instance where `intptr_t` is generated by DNNE.
* How can I use the same export name across platforms but with different implementations?
* The .NET platform provides [`SupportedOSPlatformAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.versioning.supportedosplatformattribute) and [`UnsupportedOSPlatformAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.versioning.unsupportedosplatformattribute) which are fully supported by DNNE. All .NET supplied platform names are recognized. It is also possible to define your own using `C99DeclCodeAttribute`. See [`MiscExport.cs`](./test/ExportingAssembly/MiscExports.cs) for an example.
* The .NET platform provides [`SupportedOSPlatformAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.versioning.supportedosplatformattribute) and [`UnsupportedOSPlatformAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.versioning.unsupportedosplatformattribute) which are fully supported by DNNE. All .NET supplied platform names are recognized. It is also possible to define your own using `C99DeclCodeAttribute` (or `RustDeclCodeAttribute` for Rust output). See [`MiscExport.cs`](./test/ExportingAssembly/MiscExports.cs) for an example. For C99 output, platform guards use `#ifdef`/`#ifndef` preprocessor directives. For Rust output, they use `#[cfg(...)]` attributes with flags passed via `build.rs`.
* The consuming application for my .NET assembly fails catastrophically if .NET is not installed. How can I improve this UX?
* For all non-recoverable scenarios, DNNE will call the standard C `abort()` function. This can be overridden by providing your own `dnne_abort()` function. See [`override.c`](./test/ExportingAssembly/override.c) in the [`ExportingAssembly`](./test/ExportingAssembly/ExportingAssembly.csproj) project for an example.
* For all non-recoverable scenarios, DNNE will call the standard C `abort()` function. This can be overridden by providing your own `dnne_abort()` function. See [`override.c`](./test/ExportingAssembly/override.c) in the [`ExportingAssembly`](./test/ExportingAssembly/ExportingAssembly.csproj) project for an example. The `dnne_abort()` option is not supported when using the Rust language.
* How can I add documentation to the exported function in the header file?
* Add the normal triple-slash comments to the exported functions and then set the MSBuild property `GenerateDocumentationFile` to `true` in the project. The compiler will generated xml documentation for the exported C# functions and that will be be added to the generated header file.
* How can I keep my project cross-platform and generate a native binary for other platforms than the one I am currently building on?
* The managed assembly will remain cross-platform but the native component is difficult to produce due to native tool chain constraints. In order to accomplish this on the native side, there would need to exist a C99 tool chain that can target any platform from any other platform. For example, the native tool chain could run on Windows but would need to provide a macOS SDK, linux SDK, and produce a macOS `.dylib` (Mach-O image) and/or a linux `.so` (ELF image). If such a native tool chain exists, it would be possible.
* How can I consume the resulting native binary?
* There are two options: (1) manually load the binary and discover its exports or (2) directly link against the binary. Both options are discussed in the [native sample](./sample/native/main.c).
* For C99 output, there are two options: (1) manually load the binary and discover its exports or (2) directly link against the binary. Both options are discussed in the [native sample](./sample/native/main.c).
* For Rust output, add the generated crate as a path dependency in your `Cargo.toml` and call the exports directly. See [Generating a Rust crate](#generating-a-rust-crate) and the [Rust example](./test/ImportingProcess.Rust).
* Along with exporting a function, I would also like to export data. Is there a way to export a static variable defined in .NET?
* There is no simple way to do this starting from .NET. DNNE could be updated to read static metadata and then generate the appropriate export in C code, but that approach is complicated by how static data can be defined during module load in .NET. It is recommended instead to define the desired static data in a separate translation unit (`.c` file) and include it in the native build through the `DnneCompilerUserFlags` property.
* Does DNNE support targeting .NET Framework?
Expand Down
Loading
Loading