Skip to content

Add sample for custom marshalling in p/invoke source generation #5315

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<Project>
<!-- Shared properties -->
<PropertyGroup>
<SourceRoot>$(MSBuildThisFileDirectory)src</SourceRoot>
<BinRoot>$(MSBuildThisFileDirectory)bin</BinRoot>
</PropertyGroup>
</Project>
54 changes: 54 additions & 0 deletions core/interop/source-generation/custom-marshalling/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
languages:
- csharp
- cpp
products:
- dotnet-core
page_type: sample
name: "Custom Marshalling Source Generation"
urlFragment: "custom-marshalling-source-generation"
description: "A .NET application that demonstrates using the custom marshalling mechanism in interop source generation."
---

# Custom marshalling source generation

The ability to use [source generation for P/Invokes](https://docs.microsoft.com/dotnet/standard/native-interop/pinvoke-source-generation) was introduced in .NET 7. This also included a mechanism for [custom marshalling of types](https://docs.microsoft.com/dotnet/standard/native-interop/custom-marshalling-source-generation).

This sample implements and uses custom marshallers for a built-in type and a user-defined type, including both stateless and stateful marshallers. It demonstrates the usage of attributes relevant to source generation for interop:

- [`LibraryImportAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.libraryimportattribute)
- [`CustomMarshallerAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshalling.custommarshallerattribute)
- [`MarshalUsingAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshalling.marshalusingattribute)
- [`NativeMarshallingAttribute`](https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.marshalling.nativemarshallingattribute)

## Prerequisites

- [.NET 7 SDK](https://dotnet.microsoft.com/download) Preview 7 or later

- C++ compiler
- Windows: `cl.exe`
- See [installation instructions](https://docs.microsoft.com/cpp/build/building-on-the-command-line#download-and-install-the-tools).
- Linux/macOS: `g++`

## Build and Run

1) In order to build and run, all prerequisites must be installed. The following are also required:

- On Linux/macOS, the C++ compiler (`g++`) must be on the path.
- The C++ compiler (`cl.exe` or `g++`) and `dotnet` must be the same bitness (32-bit versus 64-bit).
- On Windows, the sample is set up to use the bitness of `dotnet` to find the corresponding `cl.exe`

1) Navigate to the root directory and run `dotnet build`

1) Run the samples. Do one of the following:

- Use `dotnet run` (which will build and run at the same time).
- Use `dotnet build` to build the executable. The executable will be in `bin` under a subdirectory for the configuration (`Debug` is the default).
- Windows: `bin\Debug\custommarshalling.exe`
- Non-Windows: `bin/Debug/custommarshalling`

Note: The way the sample is built is relatively complicated. The goal is that it's possible to build and run the sample with a simple `dotnet run` command with minimal requirements for pre-installed tools. Typically, real-world projects that have both managed and native components will use different build systems for each; for example, msbuild/dotnet for managed and CMake for native.

## Visual Studio support

The `src\custommarshalling.sln` can be used to open the sample in Visual Studio 2022. In order to be able to build from Visual Studio, though, it has to be started from the correct developer environment. From the developer environment console, start it with `devenv src\custommarshalling.sln`. With that, the solution can be built. To run it, set the start project to `custommarshalling`.
15 changes: 15 additions & 0 deletions core/interop/source-generation/custom-marshalling/build.proj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.Build.Traversal">

<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>

<RunCommand>$(BinRoot)\$(Configuration)\custommarshalling</RunCommand>
<RunCommand Condition="$([MSBuild]::IsOsPlatform('Windows'))">$(BinRoot)\$(Configuration)\custommarshalling.exe</RunCommand>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="src/custommarshalling/*.csproj" />
<ProjectReference Include="src/nativelib/*.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32629.440
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "custommarshalling", "custommarshalling\custommarshalling.csproj", "{BD539154-F4AB-4001-9456-93DC25DF356F}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "nativelib.vs", "nativelib\nativelib.vs.vcxproj", "{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{44D015E0-4017-42A0-AB64-054F06F1CB3D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "nativelib", "nativelib\nativelib.csproj", "{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BD539154-F4AB-4001-9456-93DC25DF356F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Debug|x64.ActiveCfg = Debug|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Debug|x64.Build.0 = Debug|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Debug|x86.ActiveCfg = Debug|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Debug|x86.Build.0 = Debug|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Release|Any CPU.Build.0 = Release|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Release|x64.ActiveCfg = Release|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Release|x64.Build.0 = Release|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Release|x86.ActiveCfg = Release|Any CPU
{BD539154-F4AB-4001-9456-93DC25DF356F}.Release|x86.Build.0 = Release|Any CPU
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Debug|Any CPU.ActiveCfg = Debug|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Debug|Any CPU.Build.0 = Debug|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Debug|x64.ActiveCfg = Debug|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Debug|x64.Build.0 = Debug|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Debug|x86.ActiveCfg = Debug|Win32
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Debug|x86.Build.0 = Debug|Win32
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Release|Any CPU.ActiveCfg = Release|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Release|Any CPU.Build.0 = Release|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Release|x64.ActiveCfg = Release|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Release|x64.Build.0 = Release|x64
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Release|x86.ActiveCfg = Release|Win32
{BA9D31E5-D9B0-4370-9607-45612AE7C6C6}.Release|x86.Build.0 = Release|Win32
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Debug|x64.ActiveCfg = Debug|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Debug|x64.Build.0 = Debug|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Debug|x86.ActiveCfg = Debug|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Debug|x86.Build.0 = Debug|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Release|Any CPU.Build.0 = Release|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Release|x64.ActiveCfg = Release|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Release|x64.Build.0 = Release|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Release|x86.ActiveCfg = Release|Any CPU
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{26317EAD-2EC8-45D5-A35B-15FC4BA5074A} = {44D015E0-4017-42A0-AB64-054F06F1CB3D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E44A7284-FCB3-40AA-92E4-52E2641D92A9}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace CustomMarshalling
{
/// <summary>
/// User-defined type that specifies a marshaller to be used by default.
/// </summary>
[NativeMarshalling(typeof(ErrorDataMarshaller))]
internal struct ErrorData
{
public int Code;
public bool IsFatalError;
public string? Message;

public void Print()
{
Console.WriteLine(nameof(ErrorData));
Console.WriteLine($"{nameof(Code)} : {Code}");
Console.WriteLine($"{nameof(IsFatalError)} : {IsFatalError}");
Console.WriteLine($"{nameof(Message)} : {Message}");
}
}

/// <summary>
/// Marshaller for <see cref="ErrorData"/>.
/// </summary>
/// <remarks>
/// Attributes are specified such that <see cref="ErrorDataMarshaller"/> is used when marshalling by-value and in
/// parameters in managed-to-unmanaged scenarios (like P/Invoke), <see cref="ErrorDataMarshaller.Out"/> is used when
/// returning or marshalling an element of an array out, and <see cref="ErrorDataMarshaller.ThrowOnFatalErrorOut"/> is
/// used when returning or marshalling out parameters in managed-to-unmanaged scenarios (like P/Invoke). All other
/// marshalling scenarios are unsupported.
/// </remarks>
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedIn, typeof(ErrorDataMarshaller))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ManagedToUnmanagedOut, typeof(ThrowOnFatalErrorOut))]
[CustomMarshaller(typeof(ErrorData), MarshalMode.ElementOut, typeof(Out))]
internal static unsafe class ErrorDataMarshaller
{
/// <summary>
/// Unmanaged representation of <see cref="ErrorData"/>
/// </summary>
internal struct ErrorDataUnmanaged
{
public int Code;
public byte IsFatal;
public uint* Message;
}

public static ErrorDataUnmanaged ConvertToUnmanaged(ErrorData managed)
{
return new ErrorDataUnmanaged
{
Code = managed.Code,
IsFatal = (byte)(managed.IsFatalError ? 1 : 0),
Message = Utf32StringMarshaller.ConvertToUnmanaged(managed.Message),
};
}

public static void Free(ErrorDataUnmanaged unmanaged)
=> Utf32StringMarshaller.Free(unmanaged.Message);

public static class Out
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
return new ErrorData
{
Code = unmanaged.Code,
IsFatalError = unmanaged.IsFatal != 0,
Message = Utf32StringMarshaller.ConvertToManaged(unmanaged.Message)
};
}

public static void Free(ErrorDataUnmanaged unmanaged)
=> ErrorDataMarshaller.Free(unmanaged);
}

public static class ThrowOnFatalErrorOut
{
public static ErrorData ConvertToManaged(ErrorDataUnmanaged unmanaged)
{
ErrorData data = Out.ConvertToManaged(unmanaged);
if (data.IsFatalError)
throw new ExternalException(data.Message, data.Code);

return data;
}

public static void Free(ErrorDataUnmanaged unmanaged)
=> ErrorDataMarshaller.Free(unmanaged);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

namespace CustomMarshalling
{
internal partial class NativeLib
{
private const string LibName = "nativelib";

[LibraryImport(LibName)]
internal static partial void PrintString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);

[LibraryImport(LibName, StringMarshallingCustomType = typeof(Utf32StringMarshaller))]
internal static partial void PrintStrings(string[] s, int count);

[LibraryImport(LibName, StringMarshallingCustomType = typeof(Utf32StringMarshaller))]
internal static partial string ReverseString([MarshalUsing(typeof(Utf32StringMarshaller))] string s);

[LibraryImport(LibName)]
internal static partial void ReverseStringInPlace([MarshalUsing(typeof(Utf32StringMarshaller))] ref string s);

[LibraryImport(LibName)]
internal static partial void PrintErrorData(ErrorData errorData);

[LibraryImport(LibName)]
internal static partial ErrorData GetFatalErrorIfNegative(int code);

[LibraryImport(LibName)]
[return: MarshalUsing(CountElementName = "len")]
internal static partial ErrorData[] GetErrors(int[] codes, int len);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using CustomMarshalling;

Console.OutputEncoding = System.Text.Encoding.UTF8;

MarshalStringAsUtf32();
MarshalErrorData();

static void MarshalStringAsUtf32()
{
Console.WriteLine($"=== Marshal strings as UTF-32 using a custom marshaller ===");
Console.WriteLine();
string s = "Ĥȅľľő, Ŵőŕľď!";

// Marshals string using Utf32StringMarshaller.ManagedToUnmanagedIn
Console.WriteLine($"--- {nameof(NativeLib.PrintString)}: uses {nameof(Utf32StringMarshaller)}.{nameof(Utf32StringMarshaller.ManagedToUnmanagedIn)}");
NativeLib.PrintString(s);
Console.WriteLine();

string[] toPrint = new string[]
{
s,
"🄷🄴🄻🄻🄾 🅆🄾🅁🄻🄳",
"Lorem ipsum dolor sit amet"
};

// Marshals strings using Utf32StringMarshaller
Console.WriteLine($"--- {nameof(NativeLib.PrintStrings)}: uses {nameof(Utf32StringMarshaller)}");
NativeLib.PrintStrings(toPrint, toPrint.Length);
Console.WriteLine();

// Marshals strings using Utf32StringMarshaller
Console.WriteLine($"--- {nameof(NativeLib.ReverseString)}: uses {nameof(Utf32StringMarshaller)}");
Console.WriteLine($"Original: {s}");
Console.WriteLine($"Reversed: {NativeLib.ReverseString(s)}");
Console.WriteLine();

// Marshals string using Utf32StringMarshaller
Console.WriteLine($"--- {nameof(NativeLib.ReverseStringInPlace)}: uses {nameof(Utf32StringMarshaller)}");
Console.WriteLine($"Original: {s}");
NativeLib.ReverseStringInPlace(ref s);
Console.WriteLine($"Reversed: {s}");
Console.WriteLine();
}

static void MarshalErrorData()
{
Console.WriteLine($"=== Marshal user-defined type {nameof(ErrorData)} ===");
Console.WriteLine();
ErrorData errorData = new ErrorData()
{
Code = -10,
IsFatalError = true,
Message = "✗✗✗✗✗✗"
};

// Marshals ErrorData using ErrorDataMarshaller
Console.WriteLine($"--- {nameof(NativeLib.PrintErrorData)}: uses {nameof(ErrorDataMarshaller)}");
NativeLib.PrintErrorData(errorData);
Console.WriteLine();

// Marshals ErrorData using ErrorDataMarshaller.ThrowOnFatalErrorOut
Console.WriteLine($"--- {nameof(NativeLib.GetFatalErrorIfNegative)}: uses {nameof(ErrorDataMarshaller)}.{nameof(ErrorDataMarshaller.ThrowOnFatalErrorOut)}");
Console.WriteLine("Getting error data with code 0");
ErrorData ret = NativeLib.GetFatalErrorIfNegative(0);
ret.Print();
Console.WriteLine();

Console.WriteLine("Getting error data with code -1");
try
{
ret = NativeLib.GetFatalErrorIfNegative(-1);
}
catch (Exception e)
{
Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

Console.WriteLine();

// Marshals ErrorData using ErrorDataMarshaller
Console.WriteLine($"--- {nameof(NativeLib.GetErrors)}: uses {nameof(ErrorDataMarshaller)}.{nameof(ErrorDataMarshaller.Out)}");
int[] codes = new int[] { -3, 2, 5 };
ErrorData[] errors = NativeLib.GetErrors(codes, codes.Length);
foreach (ErrorData error in errors)
{
error.Print();
Console.WriteLine();
}
}
Loading