Safely Calling Win32 APIs from C# — A Practical P/Invoke Guide (DllImport / LibraryImport / CsWin32)
· Go Komura · P/Invoke, DllImport, LibraryImport, CsWin32, C#, .NET, Win32, SafeHandle, Native Interop, Windows Development, Technical Consulting
On this blog we’ve already written a number of articles touching on native interop — choosing between a C++/CLI wrapper and P/Invoke, calling a C# Native AOT DLL from C/C++, a COM bridge for calling a 64-bit DLL from a 32-bit app, and how Windows DLL name resolution works — but we hadn’t yet covered P/Invoke itself, the foundation underneath all of them, as a topic on its own.
P/Invoke has the convenience of “just declare the DLL function as extern and you can call it,” but it’s also a technology where you’re bound to get burned at least once — by string marshalling, handle lifetimes, retrieving error codes, or struct layout. This article works through the points that matter in practice, centered on LibraryImport, the default as of .NET 7.
1. The bottom line up front
- From .NET 7 onward, make
LibraryImportyour default instead ofDllImport. It generates marshalling code at compile time, so it works with Native AOT and trimming, has no runtime IL-stub generation cost, and lets you step through the generated code in the debugger. The analyzerSYSLIB1054will flag places where aDllImportshould be rewritten.12 - If you’re hand-writing signatures for Win32 APIs, consider CsWin32. Just list the function names you want to call in
NativeMethods.txt, and it generatesLibraryImportsignatures, constants, and structs from the official Win32 metadata.3 - Be explicit about
StringMarshallingfor strings, and avoidStringBuilder.StringBuildermarshalling always involves a copy into a native buffer, and it’s a mechanism that’s both inefficient and easy to get wrong around termination handling.4 - Hold handles as
SafeHandle-derived classes, not rawIntPtrvalues. This is the basic discipline of .NET native interop, preventing premature release by the GC, double release, and “handle recycling attacks.”56 - If you set
SetLastError = true, readMarshal.GetLastPInvokeError()immediately after the call. You need to capture it before any other managed code execution overwrites the error code.7 - Default structs to
LayoutKind.Sequential, and be deliberate about whether to specifyPack. The actual layout underPack = 0(the default) is a different thing from the C++ compiler’s/Zpdefault (8 bytes on x86/ARM/ARM64, 16 bytes on x64/ARM64EC), and it can even differ between .NET Framework and .NET 5+. Don’t assume “the default is bound to be correct.”8 - Manage callback (delegate) lifetimes so they aren’t collected by the GC before the native side is done with them. Hold them in a
staticfield or useGC.KeepAlive, and preferUnmanagedCallersOnlywhere possible.9 - P/Invoke, a C++/CLI wrapper, and COM interop aren’t competitors — they’re a division of labor. For a plain C interface, use P/Invoke; when C++ classes, ownership, or exceptions are involved, use a C++/CLI wrapper; when you need to cross a process boundary (such as a 32/64-bit bridge), use COM. The decision table in section 10 lays this out.
2. DllImport vs. LibraryImport — which one to use
DllImport is the long-standing mechanism: at runtime, the runtime generates an IL stub for marshalling, JIT-compiles it, and only then makes the call. Because generation happens at runtime, it doesn’t play well with configurations like Native AOT or trimming that precompile the assembly ahead of time, and the generation cost itself is non-zero.1
LibraryImport is a source generator added in .NET 7 that generates marshalling code at compile time for partial methods. Because the generated code exists as C# source, you can step through it in the debugger, and signature mistakes are caught early as build errors.1
using System.Runtime.InteropServices;
internal static partial class NativeMethods
{
[LibraryImport("nativelib", EntryPoint = "to_lower", StringMarshalling = StringMarshalling.Utf16)]
internal static partial string ToLower(string str);
}
There’s an easy-to-miss assumption baked into this string return value. The marshaller always tries to free the memory pointed to by the returned pointer after copying its contents. On Windows this uses CoTaskMemFree, so if the native side allocated that pointer with anything other than CoTaskMemAlloc (a static buffer, malloc, new[], and so on — not unusual for a C API), the marshaller will free the memory with the wrong allocator, leading to heap corruption or a crash.10 Unless the counterpart’s header or documentation explicitly states it allocates with something CoTaskMemAlloc-compatible, design the return value to come back as IntPtr rather than string, and call the corresponding free function (or whatever release procedure the counterpart requires) yourself. Having the caller allocate the buffer and pass it in (a character array in place of StringBuilder, as discussed above, or the [Out] buffer pattern discussed later) avoids introducing this kind of ownership ambiguity in the first place.
The main differences from DllImport are as follows.11
CharSethas been removed, replaced byStringMarshalling(Utf16/Utf8/ a custom one). ANSI has been dropped, and UTF-8 is now a first-class option.CallingConventionhas been replaced byUnmanagedCallConvAttribute.- There’s no equivalent to
ExactSpellingorPreserveSig. The entry point name must always be specified with exact spelling, and return-value conversion is always performed straightforwardly. - Both the class and the target method must be
partial, and the project needsAllowUnsafeBlocks.
DllImport is still needed when you rely on settings LibraryImport doesn’t support yet (for example, certain MarshalAs specifications). Since the analyzer will error out when you try to use an unsupported setting, a practical approach is: write LibraryImport first, and fall back to DllImport only if it’s rejected.11
3. CsWin32 — the option of not hand-writing signatures
Declaring Win32 APIs one by one by hand with DllImport/LibraryImport accumulates risk of getting parameter types, constant values, or struct field ordering wrong. CsWin32 (Microsoft.Windows.CsWin32) is a source generator that automatically generates the signatures, related constants, and structs for the functions you want to call, drawn from officially provided Win32 API metadata.3
Usage is simple: add the NuGet package to your project, and list the names of the functions you want to call in a text file called NativeMethods.txt.
GetDpiForWindow
SetWindowPos
CreateFileW
CloseHandle
At build time, P/Invoke signatures for these functions (including return values, parameters, and SetLastError specification) are generated. Note that by default this generates traditional DllImport-based code. If you’re targeting Native AOT or trimming, you can switch to LibraryImport-based generated source by specifying allowMarshaling: false in NativeMethods.json.3 HANDLE types come out as the appropriate SafeHandle-derived type, and strings come out with the correct CharSet/StringMarshalling, so the CharSet mix-ups and struct field ordering mistakes common in hand-written code simply can’t happen.
As we wrote in where a C++/CLI wrapper pays off, inserting a thin wrapper is effective for complex DLLs involving C++ classes, ownership, and exceptions, but if the counterpart is a plain Win32 API (or a similar C-interface DLL), automatically generating signatures with CsWin32 is the shortest, least error-prone path. You can’t use CsWin32 for your own in-house DLLs, but even then you can use the style of its generated code as a template.
4. String marshalling pitfalls
The C#, VB, and F# compilers assign CharSet.None by default to a P/Invoke declaration that doesn’t specify CharSet. CharSet.None behaves identically to CharSet.Ansi — on Windows it’s marshalled as non-Unicode (a localized code page). If the Win32 API you’re calling assumes the Unicode version (the W suffix), calling it with this default leads to garbled characters or dropped multibyte characters.12
With LibraryImport, specifying StringMarshalling.Utf16 explicitly is the standard approach. Since the ANSI option itself has been removed, the classic DllImport-era accident of “relying on the default and unintentionally getting ANSI” is structurally much less likely.11
Another pitfall is the StringBuilder parameter. It’s commonly used for APIs where “the native side writes a string buffer and returns it,” but StringBuilder marshalling always involves a copy into a native buffer, and ToString() triggers yet another allocation. If the buffer is [Out] (the default), this is an inefficient mechanism where multiple allocations stack up on every call. On top of that, it tends to misbehave when the returned buffer isn’t NUL-terminated, or is a doubly NUL-terminated string. For high-frequency calls, using a character array from ArrayPool<char> is more stable.4
An [Out] string parameter is also a specification to avoid. If the string happens to be interned, it can destabilize the runtime.4
5. Handle lifetime management — why use SafeHandle
Holding native resources like file handles, registry keys, or device handles as raw IntPtr values is a design to avoid in .NET native interop. There are three reasons.5
- Premature release of the handle by the GC. If a class implementing a finalizer holds a handle in an
IntPtrfield, a race can occur where the GC collects that object and closes the handle mid-P/Invoke-call. - Handle recycling attacks. Windows actively reuses handle values. If you keep using a stale
IntPtrwhile the handle value it once held has been reassigned to a different resource, you end up operating on an unrelated resource — a serious accident. - Leaks from asynchronous exceptions. An asynchronous interruption, such as a thread abort, occurring between acquiring a handle and storing it in a field can cause a handle leak.
SafeHandle is an abstract class designed to solve these problems. It inherits from CriticalFinalizerObject, which guarantees that its release logic runs reliably even during abnormal AppDomain termination. P/Invoke calls automatically increment and decrement the handle’s reference count, so the handle can’t be recycled while a call is in progress.5
For your own handles, inherit from something like SafeHandleZeroOrMinusOneIsInvalid in the Microsoft.Win32.SafeHandles namespace and override ReleaseHandle(). ReleaseHandle() runs in a constrained execution region where “must not fail” is assumed, so the standard practice is to keep it free of complex logic and limit it to a simple release API call. You don’t need to write your own finalizer (and in fact should avoid doing so).6
6. Error handling — SetLastError and GetLastPInvokeError
Most Win32 APIs set a thread-local error code via SetLastError on failure, which the caller reads with GetLastError. To handle this from P/Invoke, set DllImportAttribute.SetLastError (there’s a property of the same name on LibraryImport) to true.13
[LibraryImport("kernel32", EntryPoint = "SetCurrentDirectoryW", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool SetCurrentDirectoryW(string path);
There are two points worth noting here.
- Read the error code right after the call. In .NET (excluding .NET Framework), each time you call a P/Invoke with
SetLastError = true, the error information is cleared first, and only the result of that single call is retained. If you insert logging or another API call in between, it gets overwritten and lost, so grab the value the moment you detect the failure.13 - Use
Marshal.GetLastPInvokeError()rather thanMarshal.GetLastWin32Error(). As of .NET 6, the two are functionally identical, but the former is the newer, recommended name reflecting cross-platform intent.7
if (!SetCurrentDirectoryW(path))
{
int error = Marshal.GetLastPInvokeError();
throw new Win32Exception(error);
}
7. Struct marshalling — blittable types and StructLayout
Types whose bit representation is identical between .NET and native code are called “blittable,” and can be passed through as-is without conversion, which makes them fast. This includes basic types like byte, int, and long, and fixed-layout structs composed entirely of blittable value types. For blittable structs, using C#’s sizeof() is faster than Marshal.SizeOf<T>(). Conversely, bool is not blittable (the native BOOL is 4 bytes, while a C/C++ bool is 1 byte), and using it without care creates bugs where half the return value gets thrown away.14
Struct layout is controlled with StructLayoutAttribute. The default, LayoutKind.Sequential (laid out in declaration order), should be your baseline, and you only reach for LayoutKind.Explicit when you need to specify field positions explicitly, like a union.8
The Pack field is easy to overlook. According to the official documentation, the overall alignment of the type is the smaller of “the size of the largest field” and “the specified Pack value,” and each field is placed at the smaller of “its own size” and “the type’s alignment.”8 In other words, setting Pack explicitly to a small value (2 or 4, for example) makes it act as an alignment ceiling, similar to C++’s #pragma pack(N). On the other hand, the default value of 0 means “the overall alignment of the type is the size of the largest field (with no further special cap applied)” — a different rule from the default of the C++ compiler’s /Zp option (struct member alignment, which defaults to an 8-byte boundary on x86/ARM/ARM64 and a 16-byte boundary on x64/ARM64EC), and the two should not simply be treated as equivalent.15 On top of that, this default layout can even differ between .NET Framework and .NET 5+. For instance, the official documentation gives an example where a struct containing a decimal ends up 28 bytes under default packing on .NET Framework but 32 bytes on .NET 5+, due to differences in internal field composition.8 In other words, don’t assume “it must be right because it’s the default” on a per-architecture basis. If you’re dealing with a DLL whose native-side header explicitly changes the packing size via #pragma pack, or that includes a field requiring alignment greater than 8 bytes, you should either specify Pack explicitly on the C# side or verify the actual field offsets with something like Marshal.OffsetOf. Neglecting this leads to field offsets drifting out of alignment, and data getting silently corrupted. Conversely, for a straightforward API that uses Windows SDK headers as-is, where all fields are basic types of 8 bytes or smaller, leaving the default alignment alone without touching Pack almost never causes problems in practice.
// Example where the native-side header explicitly specifies pack(4)
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal struct DeviceInfo
{
public int DeviceId;
public uint Flags;
public long Timestamp;
}
8. Callback (delegate) lifetime management
It’s not unusual for a native API to accept a callback along the lines of “call this function when you’re done.” In managed code, delegate fills that role, but there’s a GC-specific pitfall here. Even after you obtain a function pointer from a delegate via Marshal.GetFunctionPointerForDelegate, the GC does not track the association between that function pointer and the delegate. If the delegate gets collected while the native side is still using that function pointer, it leads to a crash.9
Another easy-to-miss point is the calling convention. When passing a delegate to native code as a function pointer via P/Invoke, the “platform default calling convention” is used unless specified otherwise; if you want to match it explicitly, attach UnmanagedFunctionPointerAttribute to the delegate type.16 On x64/ARM/ARM64 there’s effectively only one calling convention, so this rarely causes real harm even if you don’t think about it, but on Windows x86 (32-bit), Stdcall (the Win32 API default) and Cdecl (common among Unix-derived C libraries) differ, so if the counterpart’s header uses Cdecl, leaving the default in place can lead to stack corruption.16
// Specify the calling convention explicitly. Required on x86 builds if the
// counterpart uses Cdecl
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void MyCallback(int code);
private static readonly MyCallback s_callback = OnNativeEvent; // held in a static field to fix its lifetime
// [UnmanagedFunctionPointer] governs the convention used when the callback
// itself is "invoked" — that's a separate thing from the convention of this
// call (RegisterCallback, a P/Invoke) itself.
// LibraryImport's default is the platform default (equivalent to stdcall on
// Windows), so if the counterpart is a Cdecl C DLL, this needs to be
// specified explicitly too
[LibraryImport("nativelib")]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvCdecl) })]
internal static partial void RegisterCallback(MyCallback callback);
private static void OnNativeEvent(int code)
{
// ...
}
// Caller side
RegisterCallback(s_callback);
GC.KeepAlive(s_callback); // explicitly keep alive a variable that might otherwise go out of scope right after
Holding it in a static field means it won’t be collected by the GC for the lifetime of the application. When you’re certain the native side only uses the callback for the duration of a single call (and discards the function pointer once the callback returns), a lighter-weight approach — a local variable plus GC.KeepAlive to extend its lifetime — also works.
Official best practices recommend using a static method marked with UnmanagedCallersOnlyAttribute combined with a function pointer (delegate*<...>) over the Delegate type wherever possible. It has lower overhead compared to delegate marshalling and is more compatible with Native AOT.9
9. 32-bit / 64-bit differences
Writing a single P/Invoke signature means, at runtime, the same code path gets used whether it’s called from a 32-bit process or a 64-bit process. What tends to cause trouble here is that the width of native-side types follows the bitness of the process.
- Pointer-like types such as
HANDLE,HWND, andLPARAMare 4 bytes in a 32-bit process and 8 bytes in a 64-bit process. On the .NET side, receiving them asIntPtr/UIntPtr(ornint/nuint) is correct; receiving them as a fixed-sizeint/longcreates code that only works on one of 32-bit or 64-bit.4 - If a struct contains one of these pointer-like fields, the overall size of the struct also changes depending on bitness. Combined with the fact that the default
Packvalue from section 7 differs across architectures, test with the assumption that the same struct definition can end up with a different binary layout between a 32-bit build and a 64-bit build. - The requirement of “I want to use functionality from a 64-bit-only DLL from an existing 32-bit app” is not something P/Invoke itself can solve (DLLs of different bitness cannot coexist in the same process). In this case, you need to separate the processes and bridge them with a COM bridge or named pipes. See “A COM bridge case study: calling a 64-bit DLL from a 32-bit app” for a worked example.
- The DLL not being found at all, or an unintended version getting loaded, isn’t a P/Invoke issue — it’s a Windows loader issue. “How Windows DLL name resolution works” covers search order and SxS behavior, so check that when investigating the cause of a
DllNotFoundException.
10. Decision table — P/Invoke vs. C++/CLI wrapper vs. COM interop
P/Invoke isn’t the only way to call native code from C#. If the counterpart is a complex DLL with C++ classes, ownership, and exceptions, a C++/CLI wrapper works well; if you need to cross a process boundary (a 32/64-bit bridge, or use from another language like VBA), COM becomes an option.
| Aspect | P/Invoke (LibraryImport) | C++/CLI wrapper | COM interop |
|---|---|---|---|
| Best suited to | A plain C interface (structs and primitive types) | A DLL involving C++ classes, ownership, exceptions, std:: types |
A counterpart across a process boundary, or another language like VBA |
| Implementation cost | Low to medium (just define the signature) | Medium (write one more wrapper layer) | High (interface design, registry registration) |
| Type safety | Medium (a hand-written signature error may not surface until runtime; CsWin32 improves this) | High (can work with C++ types directly) | Medium (guaranteed by the IDL/type library) |
| AOT/trimming support | Excellent (with LibraryImport) | Poor (C++/CLI doesn’t support Native AOT) | Poor |
| Exception handling | None (must judge manually via return value or HRESULT) | Excellent (C++ exceptions can be converted to .NET exceptions) | Good (HRESULT is converted to a COM exception) |
| Crossing process boundaries | No (in-process only) | No (in-process only) | Excellent (out-of-process servers are possible) |
| Ease of debugging | Good (LibraryImport’s generated code can be stepped through) | Good (both native and managed code can be debugged in VS) | Poor (issues tied to reference counting or registration are hard to track down) |
| Learning cost | Low | Medium to high (C++/CLI syntax) | High (the whole set of COM conventions) |
If the counterpart is “a C-function-based Win32 API, or your own straightforward C DLL,” go with P/Invoke (CsWin32 if possible); if “the counterpart is a C++ class and you want ownership and exceptions to flow through naturally,” go with a C++/CLI wrapper (see “Calling a native DLL from C#: C++/CLI wrapper vs. P/Invoke” for details); and if “you need to cross a process boundary, or want it usable from VBA,” go with COM — following that order keeps the decision straightforward. For the opposite direction (calling C# processing from C/C++), the setup isn’t P/Invoke but rather Native AOT’s UnmanagedCallersOnly. See “How to call a C# Native AOT DLL from C/C++.”
11. A worked example — handle operations and error handling with LibraryImport
Here’s an example combining everything covered so far. We’ll wrap OpenDevice / CloseDevice / ReadDeviceData, exposed by a fictional sensor-device SDK device.dll, including handle management via SafeHandle, compile-time marshalling via LibraryImport, and error handling via SetLastError + GetLastPInvokeError.
First, the SafeHandle-derived class holding the native handle.
using Microsoft.Win32.SafeHandles;
// Wraps a device.dll handle. Independent of GC lifetime, this prevents
// double release, recycling attacks, and premature release of the handle
internal sealed class DeviceSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// A parameterless constructor is required because this is used as the
// return type of OpenDevice
public DeviceSafeHandle() : base(ownsHandle: true)
{
}
protected override bool ReleaseHandle()
// ReleaseHandle runs in a constrained execution region that assumes
// "must not fail." Keep it to a single, simple native release call
=> DeviceNativeMethods.CloseDevice(handle);
}
Next, the P/Invoke declarations. Strings explicitly specify StringMarshalling.Utf16, and every call that can fail has SetLastError = true.
using System.Runtime.InteropServices;
internal static partial class DeviceNativeMethods
{
private const string DeviceDll = "device.dll";
// Making the handle the return value lets SafeHandle start tracking its
// lifetime the instant the call succeeds. On failure, a handle with
// IsInvalid set to true is returned
[LibraryImport(DeviceDll, EntryPoint = "OpenDevice",
StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
internal static partial DeviceSafeHandle OpenDevice(string devicePath);
// An internal API meant to be called directly from SafeHandle's
// ReleaseHandle. handle is release-only, so it's received as a raw IntPtr
[LibraryImport(DeviceDll, EntryPoint = "CloseDevice", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CloseDevice(IntPtr handle);
// buffer is an array the caller has already allocated. byte[] is blittable,
// so it gets pinned, and the native-side write happens against that same
// memory. Specifying [Out] explicitly isn't strictly required, but it's
// added to self-document the intent
[LibraryImport(DeviceDll, EntryPoint = "ReadDeviceData", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool ReadDeviceData(
DeviceSafeHandle handle,
[Out] byte[] buffer,
int bufferLength,
out int bytesRead);
}
Finally, a thin wrapper on the consuming side. The error code is captured the moment a failure is detected and wrapped in a Win32Exception before being propagated to the caller.
using System.ComponentModel;
using System.Runtime.InteropServices;
public sealed class DeviceConnection : IDisposable
{
private readonly DeviceSafeHandle _handle;
private DeviceConnection(DeviceSafeHandle handle) => _handle = handle;
public static DeviceConnection Open(string devicePath)
{
DeviceSafeHandle handle = DeviceNativeMethods.OpenDevice(devicePath);
if (handle.IsInvalid)
{
// Captured immediately on failure, before another API call can overwrite it
int error = Marshal.GetLastPInvokeError();
handle.Dispose();
throw new IOException(
$"Failed to open device: {devicePath} (Win32 error {error})",
new Win32Exception(error));
}
return new DeviceConnection(handle);
}
public byte[] Read(int maxBytes)
{
var buffer = new byte[maxBytes];
if (!DeviceNativeMethods.ReadDeviceData(_handle, buffer, buffer.Length, out int bytesRead))
{
int error = Marshal.GetLastPInvokeError();
throw new IOException($"Failed to read from device (Win32 error {error})",
new Win32Exception(error));
}
return bytesRead == buffer.Length ? buffer : buffer[..bytesRead];
}
// Just calling SafeHandle.Dispose is enough; don't write a finalizer
public void Dispose() => _handle.Dispose();
}
Code consuming DeviceConnection needs nothing more than a using block and doesn’t have to worry about leaking handle releases. The principle of what to detect where and how to translate it, in this kind of layered setup, is exactly the layer-by-layer responsibility split we wrote about in “Where should catch and logging live in exception handling?.” The key move here is translating native-layer error codes into exceptions at the P/Invoke boundary, and treating everything above that boundary as ordinary .NET exceptions.
12. Summary
Behind the convenience of “declare the DLL function and you can call it,” P/Invoke is a technology where you’re bound to get burned at least once — in string marshalling, handle lifetimes, the timing of retrieving error codes, or struct layout. On .NET 7 and later, make LibraryImport your default, and let CsWin32 generate the signatures themselves where possible. Be explicit about StringMarshalling for strings and avoid StringBuilder. Hold handles with SafeHandle. If you use SetLastError, grab the error code immediately after the call. For structs, keep in mind that the default Pack value differs across architectures. Manage callback lifetimes explicitly. Every point raised in this article is the kind of thing that “takes just a few lines to handle if you know about it, but turns into a bug that only reproduces in production if you don’t.”
And whether to push through with P/Invoke, or switch to a C++/CLI wrapper or COM, comes down to how “C-like” the counterpart DLL is, and whether you need to cross a process boundary. Requests about calling existing native assets from C#, or the reverse — calling C# assets from native code — often can’t be resolved into an optimal setup without looking at the actual header files or DLL structure, so feel free to reach out if you’re unsure.
Related articles
- Calling a native DLL from C#: C++/CLI wrapper vs. P/Invoke
- How to call a C# Native AOT DLL from C/C++
- A COM bridge case study: calling a 64-bit DLL from a 32-bit app
- How Windows DLL name resolution works — search order and SxS
Related consulting areas
KomuraSoft LLC handles technical consulting for boundary design between C# and native DLLs/Win32 APIs, development and investigation of COM components, and migration projects that bridge existing native assets with .NET.
- Technical Consulting & Design Review
- COM Component Development (Japanese page)
- Windows App Development
- Contact
References
-
Microsoft Learn, Source generation for platform invokes. On compile-time marshalling generation via LibraryImportAttribute, its difference from DllImport’s runtime IL-stub generation, and its compatibility with Native AOT/trimming. ↩ ↩2 ↩3
-
Microsoft Learn, SYSLIB diagnostics for p/invoke source generation. On the list of diagnostic IDs including analyzer SYSLIB1054, which prompts rewriting DllImport to LibraryImport. ↩
-
Microsoft Learn, Build a C# .NET app with WinUI 3 and Win32 interop. On how to introduce the C#/Win32 P/Invoke Source Generator (Microsoft.Windows.CsWin32) and the procedure for generating signatures by listing function names in NativeMethods.txt. ↩ ↩2 ↩3
-
Microsoft Learn, Native interoperability best practices. On StringBuilder marshalling always involving a copy into a native buffer and being inefficient, avoiding [Out] string arguments, and using SafeHandle while avoiding finalizers. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, SafeHandle Class. On how SafeHandle prevents premature handle release and recycling attacks, and the guaranteed release provided by CriticalFinalizerObject. ↩ ↩2 ↩3
-
Microsoft Learn, Native interoperability best practices - General guidance. On the guideline to use SafeHandle for managing unmanaged resource lifetimes and to avoid using finalizers. ↩ ↩2
-
Microsoft Learn, Marshal.GetLastPInvokeError Method. On how to retrieve the error code immediately after a P/Invoke call with SetLastError=true, and its being recommended over GetLastWin32Error from .NET 6 onward. ↩ ↩2
-
Microsoft Learn, StructLayoutAttribute.Pack Field. On the meaning of the default Pack value of 0, “the current platform’s default packing size,” and the rules for computing field alignment. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Native interoperability best practices - Prevent delegate collection with GC.KeepAlive. On the GC not tracking the association between a function pointer obtained via GetFunctionPointerForDelegate and its delegate, extending lifetime with GC.KeepAlive, and the recommendation to use UnmanagedCallersOnly. ↩ ↩2 ↩3
-
Microsoft Learn, Default Marshalling Behavior - Memory management with the interop marshaller. On the marshaller always attempting to free memory allocated by unmanaged code, that CoTaskMemFree is used on Windows, and that memory allocated by anything other than CoTaskMemAlloc must be received as IntPtr and freed manually. ↩
-
Microsoft Learn, Source generation for platform invokes - Differences from DllImport. On CharSet being replaced by StringMarshalling, using UnmanagedCallConvAttribute instead of CallingConvention, and there being no equivalent to ExactSpelling/PreserveSig. ↩ ↩2 ↩3
-
Microsoft Learn, Charsets and marshalling. On the C#, Visual Basic, and F# compilers assigning CharSet.None by default when CharSet isn’t specified, and CharSet.None behaving the same as CharSet.Ansi (marshalling as non-Unicode). ↩
-
Microsoft Learn, DllImportAttribute.SetLastError Field. On the behavior in .NET when SetLastError is set to true, including that error information is cleared on each call. ↩ ↩2
-
Microsoft Learn, Native interoperability best practices - Blittable types. On the definition of blittable types, the pitfall caused by bool not being blittable, and the benefit of using sizeof() with blittable structs. ↩
-
Microsoft Learn, /Zp (Struct Member Alignment). On the C++ compiler’s default struct member alignment being an 8-byte boundary on x86/ARM/ARM64 and a 16-byte boundary on x64/ARM64EC. ↩
-
Microsoft Learn, Unmanaged calling conventions. On Stdcall and Cdecl being different default calling conventions on Windows x86, there effectively being only one calling convention on x64/ARM/ARM64, and specifying the calling convention explicitly via UnmanagedFunctionPointerAttribute. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
Preventing Multiple Instances of a Windows App — Named Mutexes and Activating the Existing Window on a Second Launch
This article organizes the classic requirement for business Windows apps — 'don't let the same app launch twice' — around a named Mutex. ...
Calling a C# Native AOT DLL from C/C++
How to publish a C# class library as a native DLL with Native AOT and call UnmanagedCallersOnly entry points from C/C++ — when this setup...
How to Think About Windows Session Isolation — Session 0, RDP, and Running Multiple Users Concurrently
This article untangles the concept of a Windows "session," a topic that consistently confuses Windows app developers. It covers why Sessi...
Integrating Entra ID Authentication into WinForms/WPF Apps — A Practical Architecture with MSAL.NET and the WAM Broker
A practical, hands-on look at integrating Entra ID (formerly Azure AD) authentication into WinForms/WPF desktop apps: the public client m...
Date, Time, and Timezones in Business Apps — From DateTime Pitfalls to the UTC-Storage Principle and Test Design
Timestamps drift by nine hours after a server migration; only the overseas office's dates roll back to the previous day — we trace date/t...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
32-bit / 64-bit Interoperability
Topic page for 32-bit / 64-bit interoperability, native boundaries, and related Windows design decisions.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Author Profile
Profile page for the article author.
Go Komura
Representative of KomuraSoft LLC
Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.
Public links