Skip to content

Commit 9540556

Browse files
committed
Minor update: New flags, bug fixes, updated example and readme...
* Implement EnforceBlittable option to allow disabling blittability check * Implement ExplicitOnly option to only process methods which are explicitly marked with NIMA * Fix a bug that caused generator to consider DIM as candidates * Fix another name collision issue (inner implementation `__Impl`) * Update example project * Update readme
1 parent f5da5e1 commit 9540556

11 files changed

Lines changed: 492 additions & 163 deletions

File tree

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ jobs:
5151
echo "DOTNET_VERSION=${VERSION#v}" >> $GITHUB_ENV
5252
5353
- name: Restore dependencies
54-
run: dotnet restore -p:Version=$DOTNET_VERSION
54+
run: dotnet restore --no-cache -p:Version=$DOTNET_VERSION
5555

5656
- name: Build solution
5757
run: dotnet build -c Release --no-restore --no-incremental -p:Version=$DOTNET_VERSION

Example/Example.cs

Lines changed: 130 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,158 @@
1-
global using NativeInvoke; // to import our attributes in your project
1+
global using NativeInvoke; // Import our attributes in your project
2+
global using NIMA = NativeInvoke.NativeImportMethodAttribute;
23

4+
// Alias a few common Win32 types for our example (zero marshalling):
5+
global using BOOL = int; // Win32 BOOL is 4-bytes (0=false, 1=true)
6+
global using DWORD = uint; // double-word
7+
global using UINT = uint;
8+
global using HWND = nint; // window handle
9+
global using unsafe LPCSTR = sbyte*; // ANSI C string
10+
global using unsafe LPCWSTR = char*; // Wide/Unicode string; alternatively, ushort*
11+
12+
using System.Diagnostics;
313
using System.Runtime.CompilerServices;
414
using System.Runtime.InteropServices;
5-
6-
using BOOL = int; // Win32 BOOL is 4-bytes (0=false, 1=true)
7-
using DWORD = uint; // double-word
15+
using System.Runtime.Versioning;
816

917
namespace Example;
1018

11-
#if NET6_0_OR_GREATER
12-
[System.Runtime.Versioning.SupportedOSPlatform("windows")] // Optional (for clarity)
13-
#endif
14-
public interface IKernel32<TBool, TDWord>
19+
#region kernel32
20+
21+
[SupportedOSPlatform("windows")] // Optional (for clarity)
22+
public interface IKernel32<TBool, TDWord> : IKernel // Generics are supported!
1523
where TBool : unmanaged
1624
where TDWord : unmanaged
1725
{
18-
[NativeImportMethod("Beep")]
19-
TBool Boop(TDWord frequency, TDWord duration); // generic with explicit entry point
26+
[NIMA("Beep")]
27+
TBool Boop(TDWord frequency, TDWord duration); // generic with explicit entry point (ignores SymbolPrefix/Suffix)
2028

21-
[NativeImportMethod(CallingConvention = CallingConvention.StdCall)]
22-
BOOL Boop(int frequency, int duration); // calling convention override
29+
BOOL Beep(TDWord frequency, TDWord duration); // overload without attribute, resolved using method's name (respects SymbolPrefix/Suffix)
30+
31+
[NIMA(null)] // use null or empty string to skip generation
32+
void IgnoreMe();
33+
}
2334

24-
BOOL Beep(TDWord frequency, TDWord duration); // no attribute
35+
[SupportedOSPlatform("windows")] // Optional (for clarity)
36+
public interface IKernel
37+
{
38+
[NIMA(
39+
CallingConvention = CallingConvention.StdCall // explicit calling convention overrides the default
40+
)]
41+
BOOL Beep(int frequency, int duration); // resolved using method's name as entry point
2542

26-
[NativeImportMethod(null)]
27-
void IgnoreMe(); // should be skipped
43+
void SkipMe(); // this should be ignored because ExplicitOnly=true
2844
}
2945

30-
internal sealed partial record Win32
46+
#endregion
47+
48+
#region user32
49+
50+
[SupportedOSPlatform("windows")] // Optional (for clarity)
51+
public unsafe interface IUser32<TAnsiChar, TWideChar>
52+
where TAnsiChar : unmanaged
53+
where TWideChar : unmanaged
3154
{
32-
private const string LibName = "kernel32";
55+
[NIMA(EnforceBlittable = false)] // TODO/FIXME: ReadOnlySpan (ref struct) is treated as non-blittable
56+
int MessageBoxA(HWND hWnd, ReadOnlySpan<byte> lpText, ReadOnlySpan<byte> lpCaption, UINT uType);
57+
int MessageBoxA(HWND hWnd, TAnsiChar* lpText, TAnsiChar* lpCaption, UINT uType);
58+
[NIMA(EnforceBlittable = false)] // TODO/FIXME: ReadOnlySpan (ref struct) is treated as non-blittable
59+
int MessageBoxW(HWND hWnd, ReadOnlySpan<TWideChar> lpText, ReadOnlySpan<TWideChar> lpCaption, UINT uType);
60+
[NIMA("MessageBoxW")]
61+
int MessageBox(HWND hWnd, TWideChar* lpText, TWideChar* lpCaption, UINT uType);
62+
}
63+
64+
#endregion
65+
66+
internal sealed partial class Win32 // Container can be class/struct/interface/record (nesting is also supported)
67+
{
68+
private const string kernel32 = "kernel32", user32 = "user32";
69+
70+
// Made slightly verbose, for easier customization and demonstration purposes...
71+
72+
#region kernel32
3373

3474
[NativeImport(
35-
LibName, // Specify native library name
36-
Lazy = false, // Whether to use lazy or eager module loading
37-
CallingConvention = CallingConvention.Cdecl, // Define the default calling convention
38-
SymbolPrefix = "begin_", // Define common symbol prefix
39-
SymbolSuffix = "_end", // Define common symbol suffix
40-
Inherited = false // Whether to consider interface inheritance members
75+
kernel32 // Specify native library name
76+
, EnforceBlittable = true // Whether to enforce blittable type validation (applies to all methods, can be overriden per-method)
77+
, ExplicitOnly = false // Whether only methods explicitly marked with NIMA should be considered
78+
, Inherited = true // Whether to consider inherited interface methods
79+
, Lazy = false // Whether to use lazy or eager module loading
80+
, CallingConvention = CallingConvention.StdCall // Define the default calling convention (default is platform-specific, applies to all methods, can be overriden per-method)
81+
, SuppressGCTransition = false // Whether to suppress the GC transition (applies to all methods, can be overriden per-method)
82+
, SymbolPrefix = "" // Define common prefix (prepended to method name unless using explicit entry point)
83+
, SymbolSuffix = "" // Define common suffix (appended to method name unless using explicit entry point)
4184
)]
42-
//internal static partial IKernel32 Kernel32 { get; }
4385
internal static partial IKernel32<BOOL, DWORD> Kernel32 { get; }
86+
87+
[NativeImport(
88+
kernel32 // Specify native library name
89+
//, EnforceBlittable = true // Whether to enforce blittable type validation (applies to all methods, can be overriden per-method)
90+
, ExplicitOnly = true // Whether only methods explicitly marked with NIMA should be considered
91+
, Inherited = false // Whether to consider inherited interface methods
92+
, Lazy = true // Whether to use lazy or eager module loading
93+
)]
94+
internal static partial IKernel Kernel { get; }
95+
96+
#endregion
97+
98+
#region user32
99+
100+
[NativeImport(
101+
user32 // Specify native library name
102+
, Lazy = true // Whether to use lazy or eager module loading
103+
, CallingConvention = CallingConvention.StdCall // Define the default calling convention
104+
)]
105+
internal static partial IUser32<sbyte, char> User32 { get; }
106+
107+
#endregion
44108
}
45109

46-
internal static partial class Program
110+
[SupportedOSPlatform("windows")] // Optional (for clarity)
111+
internal static unsafe partial class Program
47112
{
113+
#region How you would usually do it... (the standard way, same as DllImport, this looks ugly)
114+
48115
[LibraryImport("kernel32", EntryPoint = "Beep")]
49-
private static partial BOOL PlayBeep(DWORD f, DWORD d);
116+
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
117+
private static partial BOOL PlayBeep(DWORD frequency, DWORD duration);
118+
119+
[LibraryImport("user32", EntryPoint = "MessageBoxA")]
120+
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
121+
private static partial int MessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
122+
123+
[LibraryImport("user32", EntryPoint = "MessageBoxW")]
124+
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
125+
private static partial int MessageBoxW(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
126+
127+
[LibraryImport("user32", EntryPoint = "MessageBoxW", StringMarshalling = StringMarshalling.Utf16)]
128+
[UnmanagedCallConv(CallConvs = [typeof(CallConvStdcall)])]
129+
private static partial int MessageBox(HWND hWnd, string lpText, string lpCaption, UINT uType);
130+
131+
#endregion
50132

51133
private static void Main()
52134
{
53-
Win32.Kernel32.Boop(750, 2000);
54-
PlayBeep(750, 2000);
135+
if (!OperatingSystem.IsWindows()) throw new NotSupportedException("This example is for Windows-only");
136+
137+
Debugger.Launch();
138+
139+
Win32.Kernel32.Boop(500u, 1000u);
140+
Win32.Kernel32.Beep(600, 1000); // included because Inherited is true
141+
Win32.Kernel32.Beep(700u, 1000u);
142+
143+
Win32.User32.MessageBoxA(0, "Zero allocation example, ANSI"u8, "NativeInvoke"u8, 0u);
144+
145+
Win32.User32.MessageBoxW(0, "Zero allocation example, Unicode", "NativeInvoke", 0u); // NOTE: .AsSpan() is redundant since C# 14
146+
147+
fixed (LPCWSTR text = "Allocation and pinning example, Unicode", caption = "NativeInvoke")
148+
{
149+
Win32.User32.MessageBox(0, text, caption, /*MB_ICONWARNING*/0x30u);
150+
}
151+
152+
PlayBeep(800u, 1000u);
153+
154+
MessageBox(0, "Zero allocation example, Unicode", "LibraryImport", 0u);
155+
156+
Console.ReadKey(true);
55157
}
56158
}

Example/Example.csproj

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,38 @@
1414
<IsPackable>false</IsPackable>
1515
</PropertyGroup>
1616

17-
<ItemGroup>
18-
<ProjectReference Include="..\NativeInvoke\NativeInvoke.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
19-
</ItemGroup>
17+
<PropertyGroup>
18+
<!-- Define custom configurations -->
19+
<Configurations>Debug;Release;Local;NuGet</Configurations>
20+
</PropertyGroup>
21+
22+
<PropertyGroup Condition="'$(Configuration)'=='Local'">
23+
<UsePackageRef>false</UsePackageRef>
24+
<!-- Inherits from Debug by setting these -->
25+
<Optimize>false</Optimize>
26+
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
27+
</PropertyGroup>
28+
29+
<PropertyGroup Condition="'$(Configuration)'=='NuGet'">
30+
<UsePackageRef>true</UsePackageRef>
31+
<!-- Inherits from Debug by setting these -->
32+
<Optimize>false</Optimize>
33+
<DefineConstants>$(DefineConstants);DEBUG;TRACE</DefineConstants>
34+
</PropertyGroup>
2035

21-
<!-- Include attribute source file for development/testing (simulates contentFiles behavior from NuGet package) -->
22-
<ItemGroup>
23-
<Compile Include="..\NativeInvoke\NativeImportAttribute.cs" Link="NativeInvoke\NativeImportAttribute.cs"/>
24-
</ItemGroup>
36+
<Choose>
37+
<When Condition="'$(UsePackageRef)'=='true'">
38+
<ItemGroup>
39+
<PackageReference Include="NativeInvoke" Version="*-*"/>
40+
</ItemGroup>
41+
</When>
42+
<Otherwise>
43+
<ItemGroup>
44+
<ProjectReference Include="..\NativeInvoke\NativeInvoke.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
45+
<!-- Include attribute source file for development/testing (simulates contentFiles behavior from NuGet package) -->
46+
<Compile Include="..\NativeInvoke\NativeImportAttribute.cs" Link="NativeInvoke\NativeImportAttribute.cs"/>
47+
</ItemGroup>
48+
</Otherwise>
49+
</Choose>
2550

2651
</Project>

Example/run-local.cmd

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@echo off
2+
cd /d "%~dp0"
3+
rmdir /s /q "bin" >NUL 2>&1
4+
rmdir /s /q "obj" >NUL 2>&1
5+
dotnet restore --no-cache
6+
dotnet build -c Local --no-restore --no-incremental
7+
dotnet run -c Local --no-build
8+
rem pause >NUL
9+
exit /b 0

Example/run-nuget.cmd

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@echo off
2+
cd /d "%~dp0"
3+
rmdir /s /q "bin" >NUL 2>&1
4+
rmdir /s /q "obj" >NUL 2>&1
5+
dotnet restore --no-cache
6+
dotnet build -c NuGet --no-restore --no-incremental
7+
dotnet run -c NuGet --no-build
8+
rem pause >NUL
9+
exit /b 0

NativeInvoke.slnx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
<Solution>
2-
<Project Path="Example/Example.csproj"/>
3-
<Project Path="NativeInvoke/NativeInvoke.csproj"/>
2+
<Project Path="Example/Example.csproj">
3+
<BuildType Project="Local" />
4+
</Project>
5+
<Project Path="NativeInvoke/NativeInvoke.csproj" />
46
</Solution>

0 commit comments

Comments
 (0)