diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c2e708b --- /dev/null +++ b/.github/copilot-instructions.md @@ -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 `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 `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. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 007ca58..3feda4c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,6 +23,10 @@ jobs: with: dotnet-version: '8.0.x' dotnet-quality: 'ga' + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 'stable' - name: Build Product and Package run: dotnet build src/create_package.proj -c ${{ matrix.flavor }} - name: Unit Test Product @@ -35,6 +39,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 @@ -59,6 +66,10 @@ jobs: with: dotnet-version: '8.0.x' dotnet-quality: 'ga' + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 'stable' - name: Build Product and Package run: dotnet build src\create_package.proj -c ${{ matrix.flavor }} - name: Build ExportingAssembly (.NET Core and .NET Framework) @@ -71,6 +82,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 @@ -90,6 +104,10 @@ jobs: with: dotnet-version: '8.0.x' dotnet-quality: 'ga' + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 'stable' - name: Build Product and Package run: dotnet build src/create_package.proj -c ${{ matrix.flavor }} - name: Unit Test Product @@ -98,6 +116,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 diff --git a/.gitignore b/.gitignore index ec1bab3..bf4825e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ **/bin/ **/obj/ +**/target/ **/*.user **/launchSettings.json diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..187e1f4 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,9 @@ + + + net8.0 + 2.0.8 + + $(MSBuildThisFileDirectory)/ + $(RepoRoot)src/ + + \ No newline at end of file diff --git a/readme.md b/readme.md index 4c51f7d..a2b4c93 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -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. @@ -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 @@ -151,6 +154,44 @@ In addition to providing declaration code directly, users can also supply `#incl [DNNE.C99DeclCode("#include ")] ``` +### 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 +{ + /// + /// Provide Rust code to be defined in the generated Rust source file. + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Parameter, Inherited = false)] + internal sealed class RustDeclCodeAttribute : System.Attribute + { + public RustDeclCodeAttribute(string code) { } + } + + /// + /// Define the Rust type to be used. + /// + [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. @@ -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 + + rust + + ``` + +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` — package manifest with the assembly version and nethost link directives. + * `build.rs` — build script that configures library search paths and platform cfg flags. + * `lib.rs` — crate root that re-exports the `platform` and `exports` modules. + * `platform.rs` — Rust runtime hosting layer (equivalent of `platform.c`). + * `.g.rs` — 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. @@ -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`. @@ -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)` — 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()` — Preload the .NET runtime. Calls `abort()` on failure. +* `try_preload_runtime() -> Result<(), i32>` — Preload the .NET runtime. Returns `Ok(())` on success or `Err(hresult)` on failure. +* `get_callable_managed_function(...)` / `get_fast_callable_managed_function(...)` — 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`. + ## .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—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. @@ -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? diff --git a/sample/Sample.csproj b/sample/Sample.csproj index 0098224..44c6fe2 100644 --- a/sample/Sample.csproj +++ b/sample/Sample.csproj @@ -1,7 +1,7 @@ - net8.0;net472 + $(DnneTargetFramework);net472 true sample_native true @@ -15,7 +15,7 @@ - + diff --git a/src/dnne-analyzers/AttributesGenerator.cs b/src/dnne-analyzers/AttributesGenerator.cs index 7ab906e..2a420ea 100644 --- a/src/dnne-analyzers/AttributesGenerator.cs +++ b/src/dnne-analyzers/AttributesGenerator.cs @@ -82,6 +82,44 @@ public C99TypeAttribute(string code) { } } + + /// + /// Provides Rust code to be defined early in the generated Rust source file. + /// + /// + /// This attribute is respected on an exported method declaration or on a parameter for the method. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Method | global::System.AttributeTargets.Parameter, Inherited = false)] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class RustDeclCodeAttribute : global::System.Attribute + { + /// + /// Creates a new instance with the specified parameters. + /// + /// The Rust code to be defined in the generated Rust source file. + public RustDeclCodeAttribute(string code) + { + } + } + + /// + /// Defines the Rust type to be used. + /// + /// + /// The level of indirection should be included in the supplied string. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Parameter | global::System.AttributeTargets.ReturnValue, Inherited = false)] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class RustTypeAttribute : global::System.Attribute + { + /// + /// Creates a new instance with the specified parameters. + /// + /// The Rust type to be used. + public RustTypeAttribute(string code) + { + } + } } """); }); diff --git a/src/dnne-gen/C99Emitter.cs b/src/dnne-gen/C99Emitter.cs new file mode 100644 index 0000000..4f20016 --- /dev/null +++ b/src/dnne-gen/C99Emitter.cs @@ -0,0 +1,315 @@ +// Copyright 2026 Aaron R Robinson +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Text.RegularExpressions; + +namespace DNNE +{ + internal static class C99Emitter + { + private const string SafeMacroRegEx = "[^a-zA-Z0-9_]"; + private static readonly C99TypeProvider s_typeProvider = new C99TypeProvider(); + + public static void Emit(TextWriter outputStream, string assemblyName, IEnumerable exports, IEnumerable additionalCodeStatements) + { + // Convert the assembly name into a supported string for C99 macros. + var assemblyNameMacroSafe = Regex.Replace(assemblyName, SafeMacroRegEx, "_"); + var generatedHeaderDefine = $"__DNNE_GENERATED_HEADER_{assemblyNameMacroSafe.ToUpperInvariant()}__"; + var compileAsSourceDefine = "DNNE_COMPILE_AS_SOURCE"; + + // Emit declaration preamble + outputStream.WriteLine( +$@"// +// Auto-generated by dnne-gen +// +// .NET Assembly: {assemblyName} +// + +// +// Declare exported functions +// +#ifndef {generatedHeaderDefine} +#define {generatedHeaderDefine} + +#include +#include +#ifdef {compileAsSourceDefine} + #include +#else + // When used as a header file, the assumption is + // dnne.h will be next to this file. + #include ""dnne.h"" +#endif // !{compileAsSourceDefine} +"); + + // Emit additional code statements + if (additionalCodeStatements.Any()) + { + outputStream.WriteLine( +$@"// +// Additional code provided by user +//"); + foreach (var stmt in additionalCodeStatements) + { + outputStream.WriteLine(stmt); + } + + outputStream.WriteLine(); + } + + using var implStream = new StringWriter(); + + // Emit definition preamble + implStream.WriteLine( +$@"// +// Define exported functions +// +#ifdef {compileAsSourceDefine} + +#ifdef DNNE_WINDOWS + #ifdef _WCHAR_T_DEFINED + typedef wchar_t char_t; + #else + typedef unsigned short char_t; + #endif +#else + typedef char char_t; +#endif + +// +// Forward declarations +// + +extern void* get_callable_managed_function( + const char_t* dotnet_type, + const char_t* dotnet_type_method, + const char_t* dotnet_delegate_type); + +extern void* get_fast_callable_managed_function( + const char_t* dotnet_type, + const char_t* dotnet_type_method); +"); + + // Emit string table + implStream.WriteLine( +@"// +// String constants +// +"); + int count = 1; + var map = new StringDictionary(); + foreach (var method in exports) + { + if (map.ContainsKey(method.EnclosingTypeName)) + { + continue; + } + + string id = $"t{count++}_name"; + implStream.WriteLine( +$@"#ifdef DNNE_TARGET_NET_FRAMEWORK + static const char_t* {id} = DNNE_STR(""{method.EnclosingTypeName}""); +#else + static const char_t* {id} = DNNE_STR(""{method.EnclosingTypeName}, {assemblyName}""); +#endif // !DNNE_TARGET_NET_FRAMEWORK +"); + map.Add(method.EnclosingTypeName, id); + } + + // Emit the exports + implStream.WriteLine( +@" +// +// Exports +// +"); + foreach (var export in exports) + { + (var preguard, var postguard) = GetPlatformGuards(export.Platforms); + + // Create declaration and call signature. + string delim = ""; + var declsig = new StringBuilder(); + var callsig = new StringBuilder(); + for (int i = 0; i < export.ArgumentTypes.Length; ++i) + { + var argName = export.ArgumentNames[i] ?? $"arg{i}"; + declsig.AppendFormat("{0}{1} {2}", delim, export.ArgumentTypes[i], argName); + callsig.AppendFormat("{0}{1}", delim, argName); + delim = ", "; + } + + // Special casing for void signature. + if (declsig.Length == 0) + { + declsig.Append("void"); + } + + // Special casing for void return. + string returnStatementKeyword = "return "; + if (export.ReturnType.Equals("void")) + { + returnStatementKeyword = string.Empty; + } + + string callConv = s_typeProvider.MapCallConv(export.CallingConvention); + + string classNameConstant = map[export.EnclosingTypeName]; + Debug.Assert(!string.IsNullOrEmpty(classNameConstant)); + + // Generate the acquire managed function based on the export type. + string acquireManagedFunction; + if (export.Type == ExportType.Export) + { + acquireManagedFunction = +$@"const char_t* methodName = DNNE_STR(""{export.MethodName}""); + const char_t* delegateType = DNNE_STR(""{export.EnclosingTypeName}+{export.MethodName}Delegate, {assemblyName}""); + {export.ExportName}_ptr = ({export.ReturnType}({callConv}*)({declsig}))get_callable_managed_function({classNameConstant}, methodName, delegateType);"; + + } + else + { + Debug.Assert(export.Type == ExportType.UnmanagedCallersOnly); + acquireManagedFunction = +$@"const char_t* methodName = DNNE_STR(""{export.MethodName}""); + {export.ExportName}_ptr = ({export.ReturnType}({callConv}*)({declsig}))get_fast_callable_managed_function({classNameConstant}, methodName);"; + } + + // Declare export + outputStream.WriteLine( +$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}{export.XmlDoc} +DNNE_EXTERN_C DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig}); +{postguard}"); + + // Define export in implementation stream + implStream.WriteLine( +$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName} +static {export.ReturnType} ({callConv}* {export.ExportName}_ptr)({declsig}); +DNNE_EXTERN_C DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig}) +{{ + if ({export.ExportName}_ptr == NULL) + {{ + {acquireManagedFunction} + }} + {returnStatementKeyword}{export.ExportName}_ptr({callsig}); +}} +{postguard}"); + } + + // Emit implementation closing + implStream.Write($"#endif // {compileAsSourceDefine}"); + + // Emit output closing for header and merge in implementation + outputStream.WriteLine( +$@"#endif // {generatedHeaderDefine} + +{implStream}"); + } + + private static (string preguard, string postguard) GetPlatformGuards(in PlatformSupport platformSupport) + { + var pre = new StringBuilder(); + var post = new StringBuilder(); + + var postAssembly = ConvertScope(platformSupport.Assembly, ref pre); + var postModule = ConvertScope(platformSupport.Module, ref pre); + var postType = ConvertScope(platformSupport.Type, ref pre); + var postMethod = ConvertScope(platformSupport.Method, ref pre); + + // Append the post guards in reverse order + post.Append(postMethod); + post.Append(postType); + post.Append(postModule); + post.Append(postAssembly); + + return (pre.ToString(), post.ToString()); + + static string ConvertScope(in Scope scope, ref StringBuilder pre) + { + (string pre_support, string post_support) = ConvertCollection(scope.Support, "(", ")"); + (string pre_nosupport, string post_nosupport) = ConvertCollection(scope.NoSupport, "!(", ")"); + + var post = new StringBuilder(); + if (!string.IsNullOrEmpty(pre_support) + || !string.IsNullOrEmpty(pre_nosupport)) + { + // Add the preamble for the guard + pre.Append("#if "); + post.Append("#endif // "); + + // Append the "support" clauses because if they don't exist they are string.Empty + pre.Append(pre_support); + post.Append(post_support); + + // Check if we need to chain the clauses + if (!string.IsNullOrEmpty(pre_support) && !string.IsNullOrEmpty(pre_nosupport)) + { + pre.Append(" && "); + post.Append(" && "); + } + + // Append the "nosupport" clauses because if they don't exist they are string.Empty + pre.Append($"{pre_nosupport}"); + post.Append($"{post_nosupport}"); + + pre.Append('\n'); + post.Append('\n'); + } + + return post.ToString(); + } + + static (string pre, string post) ConvertCollection(in IEnumerable platforms, in string prefix, in string suffix) + { + var pre = new StringBuilder(); + var post = new StringBuilder(); + + var delim = prefix; + foreach (OSPlatform os in platforms) + { + if (pre.Length != 0) + { + delim = " || "; + } + + var platformMacroSafe = Regex.Replace(os.ToString(), SafeMacroRegEx, "_").ToUpperInvariant(); + pre.Append($"{delim}defined({platformMacroSafe})"); + post.Append($"{post}{delim}{platformMacroSafe}"); + } + + if (pre.Length != 0) + { + pre.Append(suffix); + post.Append(suffix); + } + + return (pre.ToString(), post.ToString()); + } + } + } +} diff --git a/src/dnne-gen/Generator.cs b/src/dnne-gen/Generator.cs index c4ca8be..2c2b039 100644 --- a/src/dnne-gen/Generator.cs +++ b/src/dnne-gen/Generator.cs @@ -20,7 +20,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; @@ -29,8 +28,6 @@ using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Runtime.Versioning; -using System.Text; -using System.Text.RegularExpressions; using System.Xml; namespace DNNE @@ -48,6 +45,12 @@ public GeneratorException(string assemblyPath, string message) class Generator : IDisposable { + public enum OutputLanguage + { + C99, + Rust, + } + private bool isDisposed = false; private readonly ICustomAttributeTypeProvider typeResolver = new TypeResolver(); @@ -58,9 +61,11 @@ class Generator : IDisposable private readonly Scope moduleScope; private readonly IDictionary typePlatformScenarios = new Dictionary(); private readonly Dictionary loadedXmlDocumentation; + private readonly OutputLanguage language; - public Generator(string validAssemblyPath, string xmlDocFile) + public Generator(string validAssemblyPath, string xmlDocFile, OutputLanguage language) { + this.language = language; this.assemblyPath = validAssemblyPath; this.peReader = new PEReader(File.OpenRead(this.assemblyPath)); this.mdReader = this.peReader.GetMetadataReader(MetadataReaderOptions.None); @@ -76,17 +81,11 @@ public Generator(string validAssemblyPath, string xmlDocFile) public void Emit(string outputFile) { - var generatedCode = new StringWriter(); + using var generatedCode = new StringWriter(); Emit(generatedCode); - // Check if the file exists - if (File.Exists(outputFile)) - { - File.Delete(outputFile); - } - // Write the generated code to the output file. - using (var outputFileStream = new StreamWriter(File.OpenWrite(outputFile))) + using (var outputFileStream = new StreamWriter(File.Create(outputFile))) { outputFileStream.Write(generatedCode.ToString()); } @@ -120,9 +119,9 @@ public void Emit(TextWriter outputStream) if (currAttrType == ExportType.None) { // Check if method has other supported attributes. - if (this.TryGetC99DeclCodeAttributeValue(customAttr, out string c99Decl)) + if (this.TryGetLanguageDeclCodeAttributeValue(customAttr, out string declCode)) { - additionalCodeStatements.Add(c99Decl); + additionalCodeStatements.Add(declCode); } else if (this.TryGetOSPlatformAttributeValue(customAttr, out bool isSupported, out OSPlatform scen)) { @@ -219,11 +218,18 @@ public void Emit(TextWriter outputStream) MethodSignature signature; try { - var typeProvider = new C99TypeProvider(); - - signature = methodDef.DecodeSignature(typeProvider, null); - - typeProvider.ThrowIfUnsupportedLastPrimitiveType(); + if (this.language == OutputLanguage.Rust) + { + var typeProvider = new RustTypeProvider(); + signature = methodDef.DecodeSignature(typeProvider, null); + typeProvider.ThrowIfUnsupportedLastPrimitiveType(); + } + else + { + var typeProvider = new C99TypeProvider(); + signature = methodDef.DecodeSignature(typeProvider, null); + typeProvider.ThrowIfUnsupportedLastPrimitiveType(); + } } catch (NotSupportedTypeException nste) { @@ -255,28 +261,39 @@ public void Emit(TextWriter outputStream) foreach (var attr in param.GetCustomAttributes()) { CustomAttribute custAttr = this.mdReader.GetCustomAttribute(attr); - if (TryGetC99TypeAttributeValue(custAttr, out string c99Type)) + if (TryGetLanguageTypeAttributeValue(custAttr, out string typeOverride)) { - // Overridden type defined. if (argIndex == ReturnIndex) { - returnType = c99Type; + returnType = typeOverride; } else { Debug.Assert(argIndex >= 0); - argumentTypes[argIndex] = c99Type; + argumentTypes[argIndex] = typeOverride; } } - else if (TryGetC99DeclCodeAttributeValue(custAttr, out string c99Decl)) + else if (TryGetLanguageDeclCodeAttributeValue(custAttr, out string declCode)) { - additionalCodeStatements.Add(c99Decl); + additionalCodeStatements.Add(declCode); } } } var xmlDoc = FindXmlDoc(enclosingTypeName.Replace('+', '.') + Type.Delimiter + managedMethodName, argumentTypes); + // In Rust mode, skip exports that have non-primitive value types + // without a type override (indicated by the "/* SUPPLY TYPE */" placeholder). + if (this.language == OutputLanguage.Rust) + { + bool hasUnsuppliedType = returnType.Contains("/* SUPPLY TYPE */") + || argumentTypes.Any(t => t.Contains("/* SUPPLY TYPE */")); + if (hasUnsuppliedType) + { + continue; + } + } + exportedMethods.Add(new ExportedMethod() { Type = exportAttrType, @@ -308,7 +325,14 @@ public void Emit(TextWriter outputStream) } string assemblyName = this.mdReader.GetString(this.mdReader.GetAssemblyDefinition().Name); - EmitC99(outputStream, assemblyName, exportedMethods, additionalCodeStatements); + if (this.language == OutputLanguage.Rust) + { + RustEmitter.Emit(outputStream, assemblyName, exportedMethods, additionalCodeStatements); + } + else + { + C99Emitter.Emit(outputStream, assemblyName, exportedMethods, additionalCodeStatements); + } } private static Dictionary LoadXmlDocumentation(string xmlDocumentation) @@ -366,13 +390,6 @@ public void Dispose() this.isDisposed = true; } - private enum ExportType - { - None, - Export, - UnmanagedCallersOnly, - } - private ExportType GetExportAttributeType(CustomAttribute attribute) { if (IsAttributeType(this.mdReader, attribute, "DNNE", "ExportAttribute")) @@ -409,20 +426,22 @@ private string ComputeEnclosingTypeName(TypeDefinition typeDef) return name; } - private bool TryGetC99TypeAttributeValue(CustomAttribute attribute, out string c99Type) + private bool TryGetLanguageTypeAttributeValue(CustomAttribute attribute, out string typeValue) { - c99Type = IsAttributeType(this.mdReader, attribute, "DNNE", "C99TypeAttribute") + string attrName = this.language == OutputLanguage.Rust ? "RustTypeAttribute" : "C99TypeAttribute"; + typeValue = IsAttributeType(this.mdReader, attribute, "DNNE", attrName) ? GetFirstFixedArgAsStringValue(this.typeResolver, attribute) : null; - return !string.IsNullOrEmpty(c99Type); + return !string.IsNullOrEmpty(typeValue); } - private bool TryGetC99DeclCodeAttributeValue(CustomAttribute attribute, out string c99Decl) + private bool TryGetLanguageDeclCodeAttributeValue(CustomAttribute attribute, out string declCode) { - c99Decl = IsAttributeType(this.mdReader, attribute, "DNNE", "C99DeclCodeAttribute") + string attrName = this.language == OutputLanguage.Rust ? "RustDeclCodeAttribute" : "C99DeclCodeAttribute"; + declCode = IsAttributeType(this.mdReader, attribute, "DNNE", attrName) ? GetFirstFixedArgAsStringValue(this.typeResolver, attribute) : null; - return !string.IsNullOrEmpty(c99Decl); + return !string.IsNullOrEmpty(declCode); } private Scope GetTypeOSPlatformScope(MethodDefinition methodDef) @@ -560,326 +579,6 @@ private static bool IsAttributeType(MetadataReader reader, CustomAttribute attri return reader.StringComparer.Equals(namespaceMaybe, targetNamespace) && reader.StringComparer.Equals(nameMaybe, targetName); } - private const string SafeMacroRegEx = "[^a-zA-Z0-9_]"; - - private static void EmitC99(TextWriter outputStream, string assemblyName, IEnumerable exports, IEnumerable additionalCodeStatements) - { - // Convert the assembly name into a supported string for C99 macros. - var assemblyNameMacroSafe = Regex.Replace(assemblyName, SafeMacroRegEx, "_"); - var generatedHeaderDefine = $"__DNNE_GENERATED_HEADER_{assemblyNameMacroSafe.ToUpperInvariant()}__"; - var compileAsSourceDefine = "DNNE_COMPILE_AS_SOURCE"; - - // Emit declaration preamble - outputStream.WriteLine( -$@"// -// Auto-generated by dnne-gen -// -// .NET Assembly: {assemblyName} -// - -// -// Declare exported functions -// -#ifndef {generatedHeaderDefine} -#define {generatedHeaderDefine} - -#include -#include -#ifdef {compileAsSourceDefine} - #include -#else - // When used as a header file, the assumption is - // dnne.h will be next to this file. - #include ""dnne.h"" -#endif // !{compileAsSourceDefine} -"); - - // Emit additional code statements - if (additionalCodeStatements.Any()) - { - outputStream.WriteLine( -$@"// -// Additional code provided by user -//"); - foreach (var stmt in additionalCodeStatements) - { - outputStream.WriteLine(stmt); - } - - outputStream.WriteLine(); - } - - var implStream = new StringWriter(); - - // Emit definition preamble - implStream.WriteLine( -$@"// -// Define exported functions -// -#ifdef {compileAsSourceDefine} - -#ifdef DNNE_WINDOWS - #ifdef _WCHAR_T_DEFINED - typedef wchar_t char_t; - #else - typedef unsigned short char_t; - #endif -#else - typedef char char_t; -#endif - -// -// Forward declarations -// - -extern void* get_callable_managed_function( - const char_t* dotnet_type, - const char_t* dotnet_type_method, - const char_t* dotnet_delegate_type); - -extern void* get_fast_callable_managed_function( - const char_t* dotnet_type, - const char_t* dotnet_type_method); -"); - - // Emit string table - implStream.WriteLine( -@"// -// String constants -// -"); - int count = 1; - var map = new StringDictionary(); - foreach (var method in exports) - { - if (map.ContainsKey(method.EnclosingTypeName)) - { - continue; - } - - string id = $"t{count++}_name"; - implStream.WriteLine( -$@"#ifdef DNNE_TARGET_NET_FRAMEWORK - static const char_t* {id} = DNNE_STR(""{method.EnclosingTypeName}""); -#else - static const char_t* {id} = DNNE_STR(""{method.EnclosingTypeName}, {assemblyName}""); -#endif // !DNNE_TARGET_NET_FRAMEWORK -"); - map.Add(method.EnclosingTypeName, id); - } - - // Emit the exports - implStream.WriteLine( -@" -// -// Exports -// -"); - foreach (var export in exports) - { - (var preguard, var postguard) = GetC99PlatformGuards(export.Platforms); - - // Create declaration and call signature. - string delim = ""; - var declsig = new StringBuilder(); - var callsig = new StringBuilder(); - for (int i = 0; i < export.ArgumentTypes.Length; ++i) - { - var argName = export.ArgumentNames[i] ?? $"arg{i}"; - declsig.AppendFormat("{0}{1} {2}", delim, export.ArgumentTypes[i], argName); - callsig.AppendFormat("{0}{1}", delim, argName); - delim = ", "; - } - - // Special casing for void signature. - if (declsig.Length == 0) - { - declsig.Append("void"); - } - - // Special casing for void return. - string returnStatementKeyword = "return "; - if (export.ReturnType.Equals("void")) - { - returnStatementKeyword = string.Empty; - } - - string callConv = GetC99CallConv(export.CallingConvention); - - string classNameConstant = map[export.EnclosingTypeName]; - Debug.Assert(!string.IsNullOrEmpty(classNameConstant)); - - // Generate the acquire managed function based on the export type. - string acquireManagedFunction; - if (export.Type == ExportType.Export) - { - acquireManagedFunction = -$@"const char_t* methodName = DNNE_STR(""{export.MethodName}""); - const char_t* delegateType = DNNE_STR(""{export.EnclosingTypeName}+{export.MethodName}Delegate, {assemblyName}""); - {export.ExportName}_ptr = ({export.ReturnType}({callConv}*)({declsig}))get_callable_managed_function({classNameConstant}, methodName, delegateType);"; - - } - else - { - Debug.Assert(export.Type == ExportType.UnmanagedCallersOnly); - acquireManagedFunction = -$@"const char_t* methodName = DNNE_STR(""{export.MethodName}""); - {export.ExportName}_ptr = ({export.ReturnType}({callConv}*)({declsig}))get_fast_callable_managed_function({classNameConstant}, methodName);"; - } - - // Declare export - outputStream.WriteLine( -$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}{export.XmlDoc} -DNNE_EXTERN_C DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig}); -{postguard}"); - - // Define export in implementation stream - implStream.WriteLine( -$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName} -static {export.ReturnType} ({callConv}* {export.ExportName}_ptr)({declsig}); -DNNE_EXTERN_C DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig}) -{{ - if ({export.ExportName}_ptr == NULL) - {{ - {acquireManagedFunction} - }} - {returnStatementKeyword}{export.ExportName}_ptr({callsig}); -}} -{postguard}"); - } - - // Emit implementation closing - implStream.Write($"#endif // {compileAsSourceDefine}"); - - // Emit output closing for header and merge in implementation - outputStream.WriteLine( -$@"#endif // {generatedHeaderDefine} - -{implStream}"); - } - - private static (string preguard, string postguard) GetC99PlatformGuards(in PlatformSupport platformSupport) - { - var pre = new StringBuilder(); - var post = new StringBuilder(); - - var postAssembly = ConvertScope(platformSupport.Assembly, ref pre); - var postModule = ConvertScope(platformSupport.Module, ref pre); - var postType = ConvertScope(platformSupport.Type, ref pre); - var postMethod = ConvertScope(platformSupport.Method, ref pre); - - // Append the post guards in reverse order - post.Append(postMethod); - post.Append(postType); - post.Append(postModule); - post.Append(postAssembly); - - return (pre.ToString(), post.ToString()); - - static string ConvertScope(in Scope scope, ref StringBuilder pre) - { - (string pre_support, string post_support) = ConvertCollection(scope.Support, "(", ")"); - (string pre_nosupport, string post_nosupport) = ConvertCollection(scope.NoSupport, "!(", ")"); - - var post = new StringBuilder(); - if (!string.IsNullOrEmpty(pre_support) - || !string.IsNullOrEmpty(pre_nosupport)) - { - // Add the preamble for the guard - pre.Append("#if "); - post.Append("#endif // "); - - // Append the "support" clauses because if they don't exist they are string.Empty - pre.Append(pre_support); - post.Append(post_support); - - // Check if we need to chain the clauses - if (!string.IsNullOrEmpty(pre_support) && !string.IsNullOrEmpty(pre_nosupport)) - { - pre.Append(" && "); - post.Append(" && "); - } - - // Append the "nosupport" clauses because if they don't exist they are string.Empty - pre.Append($"{pre_nosupport}"); - post.Append($"{post_nosupport}"); - - pre.Append('\n'); - post.Append('\n'); - } - - return post.ToString(); - } - - static (string pre, string post) ConvertCollection(in IEnumerable platforms, in string prefix, in string suffix) - { - var pre = new StringBuilder(); - var post = new StringBuilder(); - - var delim = prefix; - foreach (OSPlatform os in platforms) - { - if (pre.Length != 0) - { - delim = " || "; - } - - var platformMacroSafe = Regex.Replace(os.ToString(), SafeMacroRegEx, "_").ToUpperInvariant(); - pre.Append($"{delim}defined({platformMacroSafe})"); - post.Append($"{post}{delim}{platformMacroSafe}"); - } - - if (pre.Length != 0) - { - pre.Append(suffix); - post.Append(suffix); - } - - return (pre.ToString(), post.ToString()); - } - } - - private static string GetC99CallConv(SignatureCallingConvention callConv) - { - return callConv switch - { - SignatureCallingConvention.CDecl => "DNNE_CALLTYPE_CDECL", - SignatureCallingConvention.StdCall => "DNNE_CALLTYPE_STDCALL", - SignatureCallingConvention.ThisCall => "DNNE_CALLTYPE_THISCALL", - SignatureCallingConvention.FastCall => "DNNE_CALLTYPE_FASTCALL", - SignatureCallingConvention.Unmanaged => "DNNE_CALLTYPE", - _ => throw new NotSupportedException($"Unknown CallingConvention: {callConv}"), - }; - } - - private struct PlatformSupport - { - public Scope Assembly { get; init; } - public Scope Module { get; init; } - public Scope Type { get; init; } - public Scope Method { get; init; } - } - - private struct Scope - { - public IEnumerable Support { get; init; } - public IEnumerable NoSupport { get; init; } - } - - private class ExportedMethod - { - public ExportType Type { get; init; } - public string EnclosingTypeName { get; init; } - public string MethodName { get; init; } - public string ExportName { get; init; } - public SignatureCallingConvention CallingConvention { get; init; } - public PlatformSupport Platforms { get; init; } - public string ReturnType { get; init; } - public string XmlDoc { get; init; } - public ImmutableArray ArgumentTypes { get; init; } - public ImmutableArray ArgumentNames { get; init; } - } - private enum KnownType { Unknown, @@ -969,153 +668,40 @@ public bool IsSystemType(KnownType type) return type == KnownType.SystemType; } } + } - private class UnusedGenericContext { } - - private class NotSupportedTypeException : Exception - { - public string Type { get; private set; } - public NotSupportedTypeException(string type) { this.Type = type; } - } - - private class C99TypeProvider : ISignatureTypeProvider - { - PrimitiveTypeCode? lastUnsupportedPrimitiveType; - - public string GetArrayType(string elementType, ArrayShape shape) - { - throw new NotSupportedTypeException(elementType); - } - - public string GetByReferenceType(string elementType) - { - throw new NotSupportedTypeException(elementType); - } - - public string GetFunctionPointerType(MethodSignature signature) - { - // Define the native function pointer type in a comment. - string args = this.GetPrimitiveType(PrimitiveTypeCode.Void); - if (signature.ParameterTypes.Length != 0) - { - var argsBuffer = new StringBuilder(); - var delim = ""; - foreach (var type in signature.ParameterTypes) - { - argsBuffer.Append(delim); - argsBuffer.Append(type); - delim = ", "; - } - - args = argsBuffer.ToString(); - } - - var callingConvention = GetC99CallConv(signature.Header.CallingConvention); - var typeComment = $"/* {signature.ReturnType}({callingConvention} *)({args}) */ "; - return typeComment + this.GetPrimitiveType(PrimitiveTypeCode.IntPtr); - } - - public string GetGenericInstantiation(string genericType, ImmutableArray typeArguments) - { - throw new NotSupportedTypeException($"Generic - {genericType}"); - } - - public string GetGenericMethodParameter(UnusedGenericContext genericContext, int index) - { - throw new NotSupportedTypeException($"Generic - {index}"); - } - - public string GetGenericTypeParameter(UnusedGenericContext genericContext, int index) - { - throw new NotSupportedTypeException($"Generic - {index}"); - } - - public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired) - { - throw new NotSupportedTypeException($"{modifier} {unmodifiedType}"); - } - - public string GetPinnedType(string elementType) - { - throw new NotSupportedTypeException($"Pinned - {elementType}"); - } - - public string GetPointerType(string elementType) - { - this.lastUnsupportedPrimitiveType = null; - return elementType + "*"; - } - - public string GetPrimitiveType(PrimitiveTypeCode typeCode) - { - ThrowIfUnsupportedLastPrimitiveType(); - - if (typeCode == PrimitiveTypeCode.Char) - { - // Record the current type here with the expectation - // it will be of pointer type to Char, which is supported. - this.lastUnsupportedPrimitiveType = typeCode; - return "DNNE_WCHAR"; - } - - return typeCode switch - { - PrimitiveTypeCode.SByte => "int8_t", - PrimitiveTypeCode.Byte => "uint8_t", - PrimitiveTypeCode.Int16 => "int16_t", - PrimitiveTypeCode.UInt16 => "uint16_t", - PrimitiveTypeCode.Int32 => "int32_t", - PrimitiveTypeCode.UInt32 => "uint32_t", - PrimitiveTypeCode.Int64 => "int64_t", - PrimitiveTypeCode.UInt64 => "uint64_t", - PrimitiveTypeCode.IntPtr => "intptr_t", - PrimitiveTypeCode.UIntPtr => "uintptr_t", - PrimitiveTypeCode.Single => "float", - PrimitiveTypeCode.Double => "double", - PrimitiveTypeCode.Void => "void", - _ => throw new NotSupportedTypeException(typeCode.ToString()) - }; - } - - public void ThrowIfUnsupportedLastPrimitiveType() - { - if (this.lastUnsupportedPrimitiveType.HasValue) - { - throw new NotSupportedTypeException(this.lastUnsupportedPrimitiveType.Value.ToString()); - } - } - - public string GetSZArrayType(string elementType) - { - throw new NotSupportedTypeException($"Array - {elementType}"); - } - - public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - { - return SupportNonPrimitiveTypes(rawTypeKind); - } - - public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - { - return SupportNonPrimitiveTypes(rawTypeKind); - } + internal enum ExportType + { + None, + Export, + UnmanagedCallersOnly, + } - public string GetTypeFromSpecification(MetadataReader reader, UnusedGenericContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) - { - return SupportNonPrimitiveTypes(rawTypeKind); - } + internal struct PlatformSupport + { + public Scope Assembly { get; init; } + public Scope Module { get; init; } + public Scope Type { get; init; } + public Scope Method { get; init; } + } - private static string SupportNonPrimitiveTypes(byte rawTypeKind) - { - // See https://docs.microsoft.com/dotnet/framework/unmanaged-api/metadata/corelementtype-enumeration - const byte ELEMENT_TYPE_VALUETYPE = 0x11; - if (rawTypeKind == ELEMENT_TYPE_VALUETYPE) - { - return "/* SUPPLY TYPE */"; - } + internal struct Scope + { + public IEnumerable Support { get; init; } + public IEnumerable NoSupport { get; init; } + } - throw new NotSupportedTypeException("Non-primitive"); - } - } + internal class ExportedMethod + { + public ExportType Type { get; init; } + public string EnclosingTypeName { get; init; } + public string MethodName { get; init; } + public string ExportName { get; init; } + public SignatureCallingConvention CallingConvention { get; init; } + public PlatformSupport Platforms { get; init; } + public string ReturnType { get; init; } + public string XmlDoc { get; init; } + public ImmutableArray ArgumentTypes { get; init; } + public ImmutableArray ArgumentNames { get; init; } } } diff --git a/src/dnne-gen/Program.cs b/src/dnne-gen/Program.cs index 583b3d9..dc5f6ce 100644 --- a/src/dnne-gen/Program.cs +++ b/src/dnne-gen/Program.cs @@ -36,7 +36,7 @@ static void Main(string[] args) var parsed = Parse(args); - using (var g = new Generator(parsed.AssemblyPath, parsed.XmlDocFile)) + using (var g = new Generator(parsed.AssemblyPath, parsed.XmlDocFile, parsed.Language)) { if (string.IsNullOrWhiteSpace(parsed.OutputPath)) { @@ -64,6 +64,7 @@ class ParsedArguments public string AssemblyPath { get; set; } public string OutputPath { get; set; } public string XmlDocFile { get; set; } + public Generator.OutputLanguage Language { get; set; } = Generator.OutputLanguage.C99; } class ParseException : Exception @@ -136,11 +137,26 @@ static ParsedArguments Parse(string[] args) parsed.XmlDocFile = arg; break; } + case "l": + { + if ((i + 1) == args.Length) + { + throw new ParseException(flag, "Missing language"); + } + arg = args[++i]; + parsed.Language = arg.ToLowerInvariant() switch + { + "c99" => Generator.OutputLanguage.C99, + "rust" => Generator.OutputLanguage.Rust, + _ => throw new ParseException(arg, "Unsupported language."), + }; + break; + } case "?": case "help": { throw new ParseException(flag, -@"Syntax: dnne-gen [-o | -?]+ +@"Syntax: dnne-gen [-o | -l | -?]+ -o : The output file for the generated source. The last value is used. If file exists, it will be overwritten. @@ -150,6 +166,8 @@ written to stdout. This can be activated project properties. If supplied the comments from the functions are added to the output header file. + -l : The output language for generated source. + Supported: c99 (default), rust. -? : This message. "); } diff --git a/src/dnne-gen/RustEmitter.cs b/src/dnne-gen/RustEmitter.cs new file mode 100644 index 0000000..4d1f702 --- /dev/null +++ b/src/dnne-gen/RustEmitter.cs @@ -0,0 +1,266 @@ +// Copyright 2026 Aaron R Robinson +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; + +namespace DNNE +{ + internal static class RustEmitter + { + private static readonly RustTypeProvider s_typeProvider = new RustTypeProvider(); + + // Rust keywords that are valid C# identifiers but reserved in Rust. + // C# reserved keywords (for example, 'if', 'for', 'return') are excluded + // because they cannot appear as parameter names in managed assemblies. + // See https://doc.rust-lang.org/reference/keywords.html + private static readonly HashSet s_rustKeywords = new(StringComparer.Ordinal) + { + "async", "await", "crate", "dyn", "fn", "impl", "let", "loop", + "match", "mod", "move", "mut", "pub", "self", "Self", "super", + "trait", "type", "use", "where", "yield", + // Reserved for future use + "become", "box", "final", "macro", "priv", "unsized", + }; + + /// + /// Returns a safe Rust identifier for the given name. If the name is a + /// Rust keyword it is prefixed with r#; otherwise it is returned + /// unchanged. + /// + private static string SafeRustIdentifier(string name) + => s_rustKeywords.Contains(name) ? $"r#{name}" : name; + + public static void Emit(TextWriter outputStream, string assemblyName, IEnumerable exports, IEnumerable additionalCodeStatements) + { + // Emit preamble + outputStream.WriteLine( +$@"// +// Auto-generated by dnne-gen +// +// .NET Assembly: {assemblyName} +// + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] + +use core::ffi::c_void; +use core::sync::atomic::{{AtomicPtr, Ordering}}; + +// +// Forward declarations +// + +use crate::platform::get_callable_managed_function; +use crate::platform::get_fast_callable_managed_function;"); + + // Emit additional code statements as comments + if (additionalCodeStatements.Any()) + { + outputStream.WriteLine( +$@" +// +// Additional code provided by user +//"); + foreach (var stmt in additionalCodeStatements) + { + outputStream.WriteLine(stmt); + } + } + + // Emit string table + outputStream.WriteLine( +@" +// +// String constants +//"); + int count = 1; + var map = new StringDictionary(); + foreach (var method in exports) + { + if (map.ContainsKey(method.EnclosingTypeName)) + { + continue; + } + + string id = $"T{count++}_NAME"; + var typeNameWithAssembly = $"{method.EnclosingTypeName}, {assemblyName}"; + outputStream.WriteLine( +$@" +const {id}: &[u8] = b""{typeNameWithAssembly}\0"";"); + map.Add(method.EnclosingTypeName, id); + } + + // Emit exports + outputStream.WriteLine( +@" +// +// Exports +//"); + var declsig = new StringBuilder(); + var callsig = new StringBuilder(); + var typesig = new StringBuilder(); + foreach (var export in exports) + { + string cfgGuard = GetPlatformCfg(export.Platforms); + string cfgLine = string.IsNullOrEmpty(cfgGuard) ? "" : $"{cfgGuard}\n"; + + // Create declaration and call signatures. + declsig.Clear(); + callsig.Clear(); + typesig.Clear(); + string delim = ""; + for (int i = 0; i < export.ArgumentTypes.Length; ++i) + { + var argName = SafeRustIdentifier(export.ArgumentNames[i] ?? $"arg{i}"); + declsig.AppendFormat("{0}{1}: {2}", delim, argName, export.ArgumentTypes[i]); + callsig.AppendFormat("{0}{1}", delim, argName); + typesig.AppendFormat("{0}{1}", delim, export.ArgumentTypes[i]); + delim = ", "; + } + + // Return type handling + bool isVoid = export.ReturnType == "c_void"; + string returnAnnotation = isVoid ? "" : $" -> {export.ReturnType}"; + string fnReturnAnnotation = isVoid ? "" : $" -> {export.ReturnType}"; + + string callConv = s_typeProvider.MapCallConv(export.CallingConvention); + + string classNameConstant = map[export.EnclosingTypeName]; + Debug.Assert(!string.IsNullOrEmpty(classNameConstant)); + + // Generate the acquire managed function based on the export type. + string acquireManagedFunction; + if (export.Type == ExportType.Export) + { + var delegateType = $"{export.EnclosingTypeName}+{export.MethodName}Delegate, {assemblyName}"; + acquireManagedFunction = +$@" let method_name = b""{export.MethodName}\0"".as_ptr(); + let delegate_type = b""{delegateType}\0"".as_ptr(); + let new_ptr = get_callable_managed_function({classNameConstant}.as_ptr(), method_name, delegate_type);"; + } + else + { + Debug.Assert(export.Type == ExportType.UnmanagedCallersOnly); + acquireManagedFunction = +$@" let method_name = b""{export.MethodName}\0"".as_ptr(); + let new_ptr = get_fast_callable_managed_function({classNameConstant}.as_ptr(), method_name);"; + } + + string ptrName = $"{export.ExportName}_ptr"; + + // Emit export + outputStream.WriteLine( +$@" +// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}{export.XmlDoc} +{cfgLine}static {ptrName}: AtomicPtr = AtomicPtr::new(core::ptr::null_mut()); + +{cfgLine}pub unsafe fn {export.ExportName}({declsig}){returnAnnotation} {{ + let ptr = {ptrName}.load(Ordering::Acquire); + let f: unsafe {callConv} fn({typesig}){fnReturnAnnotation} = if !ptr.is_null() {{ + core::mem::transmute(ptr) + }} else {{ +{acquireManagedFunction} + {ptrName}.store(new_ptr, Ordering::Release); + core::mem::transmute(new_ptr) + }}; + f({callsig}) +}}"); + } + } + + private static string GetPlatformCfg(in PlatformSupport platformSupport) + { + var conditions = new List(); + AddCfgConditions(platformSupport.Assembly, conditions); + AddCfgConditions(platformSupport.Module, conditions); + AddCfgConditions(platformSupport.Type, conditions); + AddCfgConditions(platformSupport.Method, conditions); + + if (conditions.Count == 0) + { + return string.Empty; + } + + if (conditions.Count == 1) + { + return $"#[cfg({conditions[0]})]"; + } + + return $"#[cfg(all({string.Join(", ", conditions)}))]"; + } + + private static void AddCfgConditions(in Scope scope, List conditions) + { + var supportList = scope.Support?.ToList() ?? new List(); + var noSupportList = scope.NoSupport?.ToList() ?? new List(); + + if (supportList.Count > 0) + { + var supported = supportList.Select(MapOSPlatformToRustCfg).ToList(); + if (supported.Count == 1) + { + conditions.Add(supported[0]); + } + else + { + conditions.Add($"any({string.Join(", ", supported)})"); + } + } + + if (noSupportList.Count > 0) + { + var unsupported = noSupportList.Select(MapOSPlatformToRustCfg).ToList(); + if (unsupported.Count == 1) + { + conditions.Add($"not({unsupported[0]})"); + } + else + { + conditions.Add($"not(any({string.Join(", ", unsupported)}))"); + } + } + } + + private static string MapOSPlatformToRustCfg(OSPlatform os) + { + var name = os.ToString(); + if (name.StartsWith("DNNE_")) + { + name = name.Substring(5); + } + + return name.ToUpperInvariant() switch + { + "WINDOWS" => @"target_os = ""windows""", + "OSX" => @"target_os = ""macos""", + "LINUX" => @"target_os = ""linux""", + "FREEBSD" => @"target_os = ""freebsd""", + _ => name.ToLowerInvariant(), + }; + } + } +} diff --git a/src/dnne-gen/TypeProvider.cs b/src/dnne-gen/TypeProvider.cs new file mode 100644 index 0000000..8628056 --- /dev/null +++ b/src/dnne-gen/TypeProvider.cs @@ -0,0 +1,255 @@ +// Copyright 2026 Aaron R Robinson +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Immutable; +using System.Reflection.Metadata; +using System.Text; + +namespace DNNE +{ + internal class UnusedGenericContext { } + + internal class NotSupportedTypeException : Exception + { + public string Type { get; private set; } + public NotSupportedTypeException(string type) { this.Type = type; } + } + + internal abstract class TypeProviderBase : ISignatureTypeProvider + { + private PrimitiveTypeCode? lastUnsupportedPrimitiveType; + + public string GetArrayType(string elementType, ArrayShape shape) + { + throw new NotSupportedTypeException(elementType); + } + + public string GetByReferenceType(string elementType) + { + throw new NotSupportedTypeException(elementType); + } + + public string GetFunctionPointerType(MethodSignature signature) + { + string args = this.GetPrimitiveType(PrimitiveTypeCode.Void); + if (signature.ParameterTypes.Length != 0) + { + var argsBuffer = new StringBuilder(); + var delim = ""; + foreach (var type in signature.ParameterTypes) + { + argsBuffer.Append(delim); + argsBuffer.Append(type); + delim = ", "; + } + + args = argsBuffer.ToString(); + } + + string callConv = MapCallConv(signature.Header.CallingConvention); + string typeComment = FormatFunctionPointerComment(signature.ReturnType, callConv, args); + return typeComment + this.GetPrimitiveType(PrimitiveTypeCode.IntPtr); + } + + public string GetGenericInstantiation(string genericType, ImmutableArray typeArguments) + { + throw new NotSupportedTypeException($"Generic - {genericType}"); + } + + public string GetGenericMethodParameter(UnusedGenericContext genericContext, int index) + { + throw new NotSupportedTypeException($"Generic - {index}"); + } + + public string GetGenericTypeParameter(UnusedGenericContext genericContext, int index) + { + throw new NotSupportedTypeException($"Generic - {index}"); + } + + public string GetModifiedType(string modifier, string unmodifiedType, bool isRequired) + { + throw new NotSupportedTypeException($"{modifier} {unmodifiedType}"); + } + + public string GetPinnedType(string elementType) + { + throw new NotSupportedTypeException($"Pinned - {elementType}"); + } + + public string GetPointerType(string elementType) + { + this.lastUnsupportedPrimitiveType = null; + return FormatPointerType(elementType); + } + + public string GetPrimitiveType(PrimitiveTypeCode typeCode) + { + ThrowIfUnsupportedLastPrimitiveType(); + + if (typeCode == PrimitiveTypeCode.Char) + { + this.lastUnsupportedPrimitiveType = typeCode; + return GetCharTypeName(); + } + + return MapPrimitiveType(typeCode); + } + + public void ThrowIfUnsupportedLastPrimitiveType() + { + if (this.lastUnsupportedPrimitiveType.HasValue) + { + throw new NotSupportedTypeException(this.lastUnsupportedPrimitiveType.Value.ToString()); + } + } + + public string GetSZArrayType(string elementType) + { + throw new NotSupportedTypeException($"Array - {elementType}"); + } + + public string GetTypeFromDefinition(MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) + { + return SupportNonPrimitiveTypes(rawTypeKind); + } + + public string GetTypeFromReference(MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) + { + return SupportNonPrimitiveTypes(rawTypeKind); + } + + public string GetTypeFromSpecification(MetadataReader reader, UnusedGenericContext genericContext, TypeSpecificationHandle handle, byte rawTypeKind) + { + return SupportNonPrimitiveTypes(rawTypeKind); + } + + protected abstract string GetCharTypeName(); + protected abstract string MapPrimitiveType(PrimitiveTypeCode typeCode); + protected abstract string FormatPointerType(string elementType); + protected abstract string FormatFunctionPointerComment(string returnType, string callConv, string args); + + internal abstract string MapCallConv(SignatureCallingConvention callConv); + + private static string SupportNonPrimitiveTypes(byte rawTypeKind) + { + // See https://docs.microsoft.com/dotnet/framework/unmanaged-api/metadata/corelementtype-enumeration + const byte ELEMENT_TYPE_VALUETYPE = 0x11; + if (rawTypeKind == ELEMENT_TYPE_VALUETYPE) + { + return "/* SUPPLY TYPE */"; + } + + throw new NotSupportedTypeException("Non-primitive"); + } + } + + internal class C99TypeProvider : TypeProviderBase + { + protected override string GetCharTypeName() => "DNNE_WCHAR"; + + protected override string FormatPointerType(string elementType) => elementType + "*"; + + protected override string FormatFunctionPointerComment(string returnType, string callConv, string args) + { + return $"/* {returnType}({callConv} *)({args}) */ "; + } + + internal override string MapCallConv(SignatureCallingConvention callConv) + { + return callConv switch + { + SignatureCallingConvention.CDecl => "DNNE_CALLTYPE_CDECL", + SignatureCallingConvention.StdCall => "DNNE_CALLTYPE_STDCALL", + SignatureCallingConvention.ThisCall => "DNNE_CALLTYPE_THISCALL", + SignatureCallingConvention.FastCall => "DNNE_CALLTYPE_FASTCALL", + SignatureCallingConvention.Unmanaged => "DNNE_CALLTYPE", + _ => throw new NotSupportedException($"Unknown CallingConvention: {callConv}"), + }; + } + + protected override string MapPrimitiveType(PrimitiveTypeCode typeCode) + { + return typeCode switch + { + PrimitiveTypeCode.SByte => "int8_t", + PrimitiveTypeCode.Byte => "uint8_t", + PrimitiveTypeCode.Int16 => "int16_t", + PrimitiveTypeCode.UInt16 => "uint16_t", + PrimitiveTypeCode.Int32 => "int32_t", + PrimitiveTypeCode.UInt32 => "uint32_t", + PrimitiveTypeCode.Int64 => "int64_t", + PrimitiveTypeCode.UInt64 => "uint64_t", + PrimitiveTypeCode.IntPtr => "intptr_t", + PrimitiveTypeCode.UIntPtr => "uintptr_t", + PrimitiveTypeCode.Single => "float", + PrimitiveTypeCode.Double => "double", + PrimitiveTypeCode.Void => "void", + _ => throw new NotSupportedTypeException(typeCode.ToString()) + }; + } + } + + internal class RustTypeProvider : TypeProviderBase + { + protected override string GetCharTypeName() => "u16"; + + protected override string FormatPointerType(string elementType) => "*mut " + elementType; + + protected override string FormatFunctionPointerComment(string returnType, string callConv, string args) + { + var retType = returnType == "c_void" ? "()" : returnType; + return $"/* unsafe {callConv} fn({args}) -> {retType} */ "; + } + + internal override string MapCallConv(SignatureCallingConvention callConv) + { + return callConv switch + { + SignatureCallingConvention.CDecl => @"extern ""C""", + SignatureCallingConvention.StdCall => @"extern ""stdcall""", + SignatureCallingConvention.ThisCall => @"extern ""thiscall""", + SignatureCallingConvention.FastCall => @"extern ""fastcall""", + SignatureCallingConvention.Unmanaged => @"extern ""C""", + _ => throw new NotSupportedException($"Unknown CallingConvention: {callConv}"), + }; + } + + protected override string MapPrimitiveType(PrimitiveTypeCode typeCode) + { + return typeCode switch + { + PrimitiveTypeCode.SByte => "i8", + PrimitiveTypeCode.Byte => "u8", + PrimitiveTypeCode.Int16 => "i16", + PrimitiveTypeCode.UInt16 => "u16", + PrimitiveTypeCode.Int32 => "i32", + PrimitiveTypeCode.UInt32 => "u32", + PrimitiveTypeCode.Int64 => "i64", + PrimitiveTypeCode.UInt64 => "u64", + PrimitiveTypeCode.IntPtr => "isize", + PrimitiveTypeCode.UIntPtr => "usize", + PrimitiveTypeCode.Single => "f32", + PrimitiveTypeCode.Double => "f64", + PrimitiveTypeCode.Void => "c_void", + _ => throw new NotSupportedTypeException(typeCode.ToString()) + }; + } + } +} diff --git a/src/dnne-gen/dnne-gen.csproj b/src/dnne-gen/dnne-gen.csproj index 221db04..717978d 100644 --- a/src/dnne-gen/dnne-gen.csproj +++ b/src/dnne-gen/dnne-gen.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + $(DnneTargetFramework) DNNE Major diff --git a/src/dnne-pkg/dnne-pkg.csproj b/src/dnne-pkg/dnne-pkg.csproj index 105304b..b4a1264 100644 --- a/src/dnne-pkg/dnne-pkg.csproj +++ b/src/dnne-pkg/dnne-pkg.csproj @@ -1,7 +1,7 @@  - net8.0 + $(DnneTargetFramework) dnne_pkg False ../pkg/ @@ -17,7 +17,7 @@ DNNE - 2.0.8 + $(DnneVersion) AaronRobinsonMSFT AaronRobinsonMSFT Package used to generated native exports for .NET assemblies. diff --git a/src/msbuild/DNNE.BuildTasks/CreateCompileCommand.cs b/src/msbuild/DNNE.BuildTasks/CreateCompileCommand.cs index 413bb62..3cfb151 100644 --- a/src/msbuild/DNNE.BuildTasks/CreateCompileCommand.cs +++ b/src/msbuild/DNNE.BuildTasks/CreateCompileCommand.cs @@ -66,6 +66,9 @@ public class CreateCompileCommand : Task [Required] public string TargetFramework { get; set; } + [Required] + public string Language { get; set; } + // Optional public string UserDefinedCompilerFlags { get; set; } @@ -78,6 +81,9 @@ public class CreateCompileCommand : Task // Optional public bool IsSelfContained { get; set; } = false; + // Optional + public string AssemblyVersion { get; set; } + // Used to ensure the supplied path is absolute and // can be supplied as-is in a command line scenario. internal string AbsoluteExportsDefFilePath @@ -139,32 +145,49 @@ public override bool Execute() TargetFramework:{TargetFramework} "); - string command; - string commandArguments; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Windows.ConstructCommandLine(this, out command, out commandArguments); - } - else + string command = string.Empty; + string commandArguments = string.Empty; + if (Language.Equals("rust", StringComparison.OrdinalIgnoreCase)) { if (IsTargetingNetFramework) { - throw new NotSupportedException(".NET Framework can only be targeted on Windows"); + throw new NotSupportedException("Rust language is not supported when targeting .NET Framework. Use a .NET (Core) target framework instead."); } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - Linux.ConstructCommandLine(this, out command, out commandArguments); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + // Rust: generate a Cargo crate instead of compiling. + Rust.GenerateCrate(this); + } + else if (Language.Equals("c99", StringComparison.OrdinalIgnoreCase)) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - macOS.ConstructCommandLine(this, out command, out commandArguments); + Windows.ConstructCommandLine(this, out command, out commandArguments); } else { - throw new NotSupportedException("Unknown native build environment"); + if (IsTargetingNetFramework) + { + throw new NotSupportedException(".NET Framework can only be targeted on Windows"); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Linux.ConstructCommandLine(this, out command, out commandArguments); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + macOS.ConstructCommandLine(this, out command, out commandArguments); + } + else + { + throw new NotSupportedException("Unknown native build environment"); + } } } + else + { + throw new NotSupportedException($"Language '{Language}' is not supported"); + } this.Command = command; this.CommandArguments = commandArguments; diff --git a/src/msbuild/DNNE.BuildTasks/Rust.cs b/src/msbuild/DNNE.BuildTasks/Rust.cs new file mode 100644 index 0000000..fe04a20 --- /dev/null +++ b/src/msbuild/DNNE.BuildTasks/Rust.cs @@ -0,0 +1,132 @@ +// Copyright 2026 Aaron R Robinson +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using Microsoft.Build.Framework; + +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace DNNE.BuildTasks +{ + public class Rust + { + public static void GenerateCrate(CreateCompileCommand export) + { + export.Report(MessageImportance.Low, $"Generating Rust crate for {export.AssemblyName}"); + + var crateDir = Path.GetDirectoryName(export.Source); + + // Cargo requires semver (major.minor.patch). + var version = "1.0.0"; + if (!string.IsNullOrEmpty(export.AssemblyVersion)) + { + var parts = export.AssemblyVersion.Split('.'); + version = parts.Length >= 3 + ? $"{parts[0]}.{parts[1]}.{parts[2]}" + : export.AssemblyVersion; + } + + // Generate Cargo.toml + var cargoToml = new StringBuilder(); + cargoToml.AppendLine(@$"[package] +name = ""{export.OutputName.ToLowerInvariant()}"" +version = ""{version}"" +edition = ""2021"" + +[lib] +path = ""lib.rs"" + +[lints.rust] +unexpected_cfgs = ""allow"" +"); + File.WriteAllText(Path.Combine(crateDir, "Cargo.toml"), cargoToml.ToString()); + + // Generate lib.rs + var libRs = new StringBuilder(); + libRs.AppendLine(@$" +pub(crate) const DNNE_ASSEMBLY_NAME: &str = ""{export.AssemblyName}""; +pub mod platform; +#[path = ""{export.AssemblyName}.g.rs""] +pub mod exports; +"); + File.WriteAllText(Path.Combine(crateDir, "lib.rs"), libRs.ToString()); + + // Generate build.rs + var buildRs = new StringBuilder(); + buildRs.AppendLine(@$"fn main() {{ + println!(""cargo:rustc-link-search={export.NetHostPath.Replace("\\", "/")}""); + println!(""cargo:rustc-link-lib=static=nethost"");"); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + buildRs.AppendLine(" println!(\"cargo:rustc-link-lib=c++\");"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + buildRs.AppendLine(" println!(\"cargo:rustc-link-lib=stdc++\");"); + } + + // Emit user-defined --cfg flags + if (!string.IsNullOrEmpty(export.UserDefinedCompilerFlags)) + { + var tokens = export.UserDefinedCompilerFlags.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < tokens.Length; i++) + { + var token = tokens[i]; + + // Handle `--cfg ` + if (string.Equals(token, "--cfg", StringComparison.Ordinal)) + { + if (i + 1 < tokens.Length) + { + var cfgName = tokens[++i]; + if (!string.IsNullOrEmpty(cfgName)) + { + buildRs.AppendLine($" println!(\"cargo:rustc-cfg={cfgName}\");"); + } + } + + continue; + } + + // Handle `--cfg=` + const string cfgPrefix = "--cfg="; + if (token.StartsWith(cfgPrefix, StringComparison.Ordinal)) + { + var cfgName = token.Substring(cfgPrefix.Length); + if (!string.IsNullOrEmpty(cfgName)) + { + buildRs.AppendLine($" println!(\"cargo:rustc-cfg={cfgName}\");"); + } + + continue; + } + + // Ignore any other flags; only --cfg options are translated to cargo:rustc-cfg + } + } + + buildRs.AppendLine("}"); + File.WriteAllText(Path.Combine(crateDir, "build.rs"), buildRs.ToString()); + } + } +} diff --git a/src/msbuild/DNNE.props b/src/msbuild/DNNE.props index 9cd22a1..8dd1662 100644 --- a/src/msbuild/DNNE.props +++ b/src/msbuild/DNNE.props @@ -26,6 +26,10 @@ DNNE.props true + + + true diff --git a/src/msbuild/DNNE.targets b/src/msbuild/DNNE.targets index 70b144d..14b6740 100644 --- a/src/msbuild/DNNE.targets +++ b/src/msbuild/DNNE.targets @@ -29,6 +29,8 @@ DNNE.targets $(DnneNativeBinaryName) + c99 + NE $(TargetName)$(DnneNativeBinarySuffix) @@ -52,6 +54,9 @@ DNNE.targets .dylib .so + + + false true @@ -67,7 +72,11 @@ DNNE.targets $(DnneRuntimeIdentifier) $(DnneGeneratedBinPath)/$(DnneNativeExportsBinaryName)$(DnneNativeBinaryExt) - $(DnneGeneratedOutputPath)/$(TargetName).g.c + + + .g.rs + .g.c + $(DnneGeneratedOutputPath)/$(TargetName)$(DnneGeneratedSourceFileExt) @@ -75,33 +84,66 @@ DNNE.targets Include="$(DnneGeneratedSourceFileName)" Condition="'$(DnneGenerateExports)' == 'true'" /> + + Condition="'$(DnneGenerateExports)' == 'true' AND '$(DnneLanguage)' == 'c99'" > $(DnneNativeExportsBinaryName).h + Condition="'$(DnneGenerateExports)' == 'true' AND '$(DnneLanguage)' == 'c99'" > dnne.h + Condition="'$(DnneBuildExports)' == 'true' AND '$(DnneLanguage)' == 'c99'" > $(DnneNativeExportsBinaryName)$(DnneNativeBinaryExt) - + + Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$(DnneBuildExports)' == 'true' AND '$(DnneLanguage)' == 'c99'" > $(DnneNativeExportsBinaryName).lib + + + Cargo.toml + + + + lib.rs + + + + build.rs + + + + platform.rs + + + + $(TargetName)$(DnneGeneratedSourceFileExt) + + + -d "$(DocumentationFile)" - + @@ -176,7 +218,9 @@ DNNE.targets + + + + + $(DnneCompilerCommand) @@ -204,17 +258,39 @@ DNNE.targets WorkingDirectory="$(DnneGeneratedOutputPath)" Outputs="$(DnneCompiledToBinPath)" ConsoleToMSBuild="true" /> - - + + + + + + + + + + + + + + + + - + + - $(DnneCompilerUserFlags) $(MSBuildThisFileDirectory)override.c + $(DnneCompilerUserFlags) $(MSBuildThisFileDirectory)override.c + + + --cfg set_assembly_platform --cfg set_module_platform --cfg set_type_platform --cfg __set_platform__ --cfg set_method_platform false diff --git a/test/ImportingProcess.Rust/Cargo.lock b/test/ImportingProcess.Rust/Cargo.lock new file mode 100644 index 0000000..e886e1d --- /dev/null +++ b/test/ImportingProcess.Rust/Cargo.lock @@ -0,0 +1,14 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "exportingassemblyne" +version = "1.0.0" + +[[package]] +name = "importing-process-rust" +version = "0.1.0" +dependencies = [ + "exportingassemblyne", +] diff --git a/test/ImportingProcess.Rust/Cargo.toml b/test/ImportingProcess.Rust/Cargo.toml new file mode 100644 index 0000000..1a70a57 --- /dev/null +++ b/test/ImportingProcess.Rust/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "importing-process-rust" +version = "0.1.0" +edition = "2021" + +# The test/test.proj handles adding the crate dependency + diff --git a/test/ImportingProcess.Rust/src/main.rs b/test/ImportingProcess.Rust/src/main.rs new file mode 100644 index 0000000..ef2d12c --- /dev/null +++ b/test/ImportingProcess.Rust/src/main.rs @@ -0,0 +1,60 @@ +// Copyright 2026 Aaron R Robinson +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is furnished +// to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +//! Example Rust application that consumes .NET exports via DNNE. +//! +//! Build and run: +//! 1. Build the ExportingAssembly with Rust output: +//! dotnet build ../ExportingAssembly -p:DnneLanguage=rust +//! 2. Build and run this project: +//! cargo run + +use exportingassemblyne::platform::{self, FailureType}; +use exportingassemblyne::exports; + +fn on_failure(failure_type: FailureType, error_code: i32) { + eprintln!( + "FAILURE: Type: {:?}, Error code: {:#010x}", + failure_type, error_code + ); +} + +fn main() { + // Set failure callback. + platform::set_failure_callback(Some(on_failure)); + + // Preload the .NET runtime. + unsafe { + let result = platform::try_preload_runtime(); + assert!(result.is_ok(), "try_preload_runtime failed: {:#010x}", result.unwrap_err()); + println!("Runtime loaded successfully."); + } + + // Call .NET exports. + unsafe { + let a: i32 = 3; + let b: i32 = 5; + + let c = exports::IntIntInt(a, b); + println!("IntIntInt({}, {}) = {}", a, b, c); + + let c = exports::UnmanagedIntIntInt(a, b); + println!("UnmanagedIntIntInt({}, {}) = {}", a, b, c); + } +} diff --git a/test/test.proj b/test/test.proj index 721e7f4..1e6eb2d 100644 --- a/test/test.proj +++ b/test/test.proj @@ -1,24 +1,39 @@ + + + + Debug + true + $(MSBuildThisFileDirectory)build $(MSBuildThisFileDirectory)../src/dnne-pkg $(MSBuildThisFileDirectory)ExportingAssembly $(MSBuildThisFileDirectory)ImportingProcess + $(MSBuildThisFileDirectory)ImportingProcess.Rust + --release - - + + - - + + + + + + + + + \ No newline at end of file