|
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; |
2 | 3 |
|
| 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; |
3 | 13 | using System.Runtime.CompilerServices; |
4 | 14 | 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; |
8 | 16 |
|
9 | 17 | namespace Example; |
10 | 18 |
|
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! |
15 | 23 | where TBool : unmanaged |
16 | 24 | where TDWord : unmanaged |
17 | 25 | { |
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) |
20 | 28 |
|
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 | +} |
23 | 34 |
|
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 |
25 | 42 |
|
26 | | - [NativeImportMethod(null)] |
27 | | - void IgnoreMe(); // should be skipped |
| 43 | + void SkipMe(); // this should be ignored because ExplicitOnly=true |
28 | 44 | } |
29 | 45 |
|
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 |
31 | 54 | { |
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 |
33 | 73 |
|
34 | 74 | [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) |
41 | 84 | )] |
42 | | - //internal static partial IKernel32 Kernel32 { get; } |
43 | 85 | 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 |
44 | 108 | } |
45 | 109 |
|
46 | | -internal static partial class Program |
| 110 | +[SupportedOSPlatform("windows")] // Optional (for clarity) |
| 111 | +internal static unsafe partial class Program |
47 | 112 | { |
| 113 | + #region How you would usually do it... (the standard way, same as DllImport, this looks ugly) |
| 114 | + |
48 | 115 | [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 |
50 | 132 |
|
51 | 133 | private static void Main() |
52 | 134 | { |
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); |
55 | 157 | } |
56 | 158 | } |
0 commit comments