Native Interop: Declare and P/Invoke
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example here starts with
Option Strict On and was built with warnings treated as errors. The examples that call the C library (libc) are complete console
programs that were run in the .NET 10 SDK container (Ubuntu Linux, arm64) with root namespace ShelfApp. The examples that call Windows
DLLs (kernel32, user32) were built with EnableWindowsTargeting but could not be run on the machine that produced this page; they are
labelled "built, not run" and their behaviour is described from the documentation, not from a run. Messages quoted as compiler or
runtime output were printed by the SDK or the runtime.
Sooner or later a program has to call something that is not .NET: an operating-system function, a vendor’s hardware library, a codec, a
legacy C DLL. The mechanism is platform invoke, usually called P/Invoke: the runtime loads a native library, finds an exported function by
name, and calls it, translating ("marshalling") arguments and results at the boundary. Visual Basic has two ways to ask for it. One is its own
Declare statement, inherited from Visual Basic 6.0 and still compiled today; the other is the <DllImport> attribute, shared with every
.NET language. They produce the same kind of call, but they do not default to the same settings, and one default of Declare does not work
on Linux at all. The C# view of the same machinery is on Native Interop (C#); the
native side of the boundary (headers, calling conventions, struct layout) is on the C Reference.
COM, the other big interop mechanism on Windows, has its own page: COM Interop and Office Automation.
Four ways to reach native code
| Mechanism | What you write | Available in Visual Basic | Notes |
|---|---|---|---|
|
|
yes, Visual Basic only |
compiles to a P/Invoke method; implicitly |
|
a method with an empty body carrying the attribute |
yes |
the same attribute C# and F# use; every field is explicit and defaults to off |
|
a |
no: the generator only runs for C#; a Visual Basic |
|
Function pointers |
|
no: BC37316; use |
Source: Platform Invoke (P/Invoke)
The rest of the page follows a call from the declaration to the failure modes: first the two declaration forms and what the compiler emits for them, then libraries and names, the data types, strings, structures, callbacks, memory, handles and errors, and finally the Windows examples and the platform guards that keep them out of a Linux build.
The Declare statement
[ <attributelist> ] [ accessmodifier ] [ Shadows ] [ Overloads ]
Declare [ Ansi | Unicode | Auto ] [ Sub | Function ] name Lib "libname"
[ Alias "aliasname" ] [ ( parameterlist ) ] [ As returntype ]
A Declare statement only describes the native function; it contains no code. The rules are:
-
It may appear only at module level: in a class, a structure or a module, never in a namespace, an interface or a procedure. A generic class cannot contain one either.
-
It is implicitly
Shared. WritingShared,Overrides,Overridable,MustOverride,ImplementsorHandlesis an error (BC30244 forSharedandOverrides, the two that were tried). -
Its default accessibility is
Public, as for any member of a module; most real code writesPrivateorFriendand wraps the call. -
Libnames the library and is mandatory (BC30218 when it is missing). UnderOption Strict Onthe parameters and the return type needAsclauses (BC30211, BC30210). Note thatLibis a reserved word in Visual Basic, so a variable namedlibis a compile error (BC30183). -
Aliasgives the exported name when the Visual Basic name differs, which is useful when the native name is a Visual Basic keyword, differently cased, or starts with an underscore;Alias "#123"selects an export by ordinal number. -
The character set modifier is
Ansi(the default),UnicodeorAuto. -
The compiler does not look inside the library. A wrong parameter list is not diagnosed; it fails or corrupts memory when the call runs.
The first program declares four functions of the C library, three with Declare and one with the attribute:
Option Strict On
Imports System
Imports System.Runtime.InteropServices
Namespace E01
Friend Module Program
' Declare statement: Lib names the library, Alias names the entry point
Private Declare Function GetProcessId Lib "libc" Alias "getpid" () As Integer
Private Declare Function AbsoluteValue Lib "libc.so.6" Alias "abs" (value As Integer) As Integer
Private Declare Function StringLength Lib "libc" Alias "strlen" (<MarshalAs(UnmanagedType.LPStr)> text As String) As UIntPtr
' The same thing with the attribute: an empty body, in a class or module
<DllImport("libc", EntryPoint:="labs")>
Private Function LongAbs(x As Long) As Long
End Function
Friend Sub Main()
Console.WriteLine("getpid() > 0: " & (GetProcessId() > 0).ToString())
Console.WriteLine("abs(-42): " & AbsoluteValue(-42).ToString())
Console.WriteLine("labs(-7): " & LongAbs(-7L).ToString())
Console.WriteLine("strlen(shelf): " & StringLength("shelf").ToString())
End Sub
End Module
End Namespace
getpid() > 0: True
abs(-42): 42
labs(-7): 7
strlen(shelf): 5
Source: Declare Statement
Two things in that listing are worth noticing. The library is called "libc" in some declarations and "libc.so.6" in another, and both worked in
the container; Finding the library explains why. And the strlen declaration carries <MarshalAs(UnmanagedType.LPStr)> on its String
parameter, which is the subject of the next section.
What the compiler emits
The compiler turns every Declare into an ordinary static method with a DllImportAttribute. The attribute’s fields differ from a hand-written
one, and they can be read back with reflection. This program declares the same function in all four forms and prints the fields:
Option Strict On
Imports System
Imports System.Reflection
Imports System.Runtime.InteropServices
Namespace E09
Friend Module Program
Private Declare Function ByDefault Lib "libc" Alias "getpid" () As Integer
Private Declare Ansi Function ByAnsi Lib "libc" Alias "getpid" () As Integer
Private Declare Unicode Function ByUnicode Lib "libc" Alias "getpid" () As Integer
Private Declare Auto Function ByAuto Lib "libc" Alias "getpid" () As Integer
<DllImport("libc", EntryPoint:="getpid")>
Private Function ByAttribute() As Integer
End Function
Friend Sub Main()
Console.WriteLine("{0,-12} {1,-8} {2,-6} {3,-9} {4,-8} {5}", "name", "CharSet", "Exact", "SetLast", "Preserve", "Convention")
For Each name As String In New String() {"ByDefault", "ByAnsi", "ByUnicode", "ByAuto", "ByAttribute"}
Dim m As MethodInfo = GetType(Program).GetMethod(name, BindingFlags.NonPublic Or BindingFlags.Static)
Dim d As DllImportAttribute = m.GetCustomAttribute(Of DllImportAttribute)()
Console.WriteLine("{0,-12} {1,-8} {2,-6} {3,-9} {4,-8} {5}", name, d.CharSet, d.ExactSpelling, d.SetLastError, d.PreserveSig, d.CallingConvention)
Next
End Sub
End Module
End Namespace
name CharSet Exact SetLast Preserve Convention
ByDefault Ansi True True True Winapi
ByAnsi Ansi True True True Winapi
ByUnicode Unicode True True True Winapi
ByAuto Auto False True True Winapi
ByAttribute None False False True Winapi
Source: DllImportAttribute
| Field | Declare |
<DllImport> without arguments |
|---|---|---|
|
|
|
|
|
|
|
always |
|
|
|
|
|
|
|
SetLastError:=True on every Declare means the runtime saves the native error code (GetLastError on Windows, errno elsewhere) right
after the call, at a small cost; it can be read with Marshal.GetLastPInvokeError(). With <DllImport> you opt in yourself.
CallingConvention.Winapi means "stdcall on 32-bit Windows, the platform convention elsewhere", so a Declare cannot call a cdecl function
on 32-bit Windows; use <DllImport(CallingConvention:=CallingConvention.Cdecl)> for that. On 64-bit Windows, Linux and macOS there is only one
convention and the difference disappears.
The ByVal String trap
ByVal text As String is the natural way to pass a C string, and it is the form that every Visual Basic 6.0 declaration used. In a Declare
statement it compiles but does not mean what it says. The next program declares strlen in a few ways and asks reflection what the compiler produced
for the parameter. It also calls the first form, to see what happens:
Option Strict On
Imports System
Imports System.Reflection
Imports System.Runtime.InteropServices
Namespace E02
Friend Module Program
Private Declare Ansi Function LenAnsi Lib "libc" Alias "strlen" (ByVal text As String) As UIntPtr
Private Declare Function LenMarshalled Lib "libc" Alias "strlen" (<MarshalAs(UnmanagedType.LPStr)> ByVal text As String) As UIntPtr
Private Declare Unicode Function LenUnicode Lib "libc" Alias "strlen" (<MarshalAs(UnmanagedType.LPWStr)> ByVal text As String) As UIntPtr
Private Declare Auto Function LenAuto Lib "libc" Alias "strlen" (<MarshalAs(UnmanagedType.LPTStr)> ByVal text As String) As UIntPtr
Private Class Native
<DllImport("libc", EntryPoint:="strlen")>
Friend Shared Function LenDefault(text As String) As UIntPtr
End Function
<DllImport("libc", EntryPoint:="strlen", CharSet:=CharSet.Ansi)>
Friend Shared Function LenAnsi(text As String) As UIntPtr
End Function
<DllImport("libc", EntryPoint:="strlen", CharSet:=CharSet.Unicode)>
Friend Shared Function LenUnicode(text As String) As UIntPtr
End Function
<DllImport("libc", EntryPoint:="strlen", CharSet:=CharSet.Auto)>
Friend Shared Function LenAuto(text As String) As UIntPtr
End Function
End Class
Friend Sub Main()
For Each name As String In New String() {"LenAnsi", "LenMarshalled", "LenUnicode", "LenAuto"}
Dim m As MethodInfo = GetType(Program).GetMethod(name, BindingFlags.NonPublic Or BindingFlags.Static)
Dim p As ParameterInfo = m.GetParameters()(0)
Dim ma As MarshalAsAttribute = p.GetCustomAttribute(Of MarshalAsAttribute)()
Console.WriteLine(name & ": " & p.ParameterType.ToString() & " marshal=" & If(ma Is Nothing, "none", ma.Value.ToString()))
Next
Console.WriteLine("Declare LPStr: " & LenMarshalled("héllo").ToString())
Console.WriteLine("Declare LPWStr: " & LenUnicode("héllo").ToString())
Console.WriteLine("Declare LPTStr: " & LenAuto("héllo").ToString())
Console.WriteLine("DllImport none: " & Native.LenDefault("héllo").ToString())
Console.WriteLine("DllImport Ansi: " & Native.LenAnsi("héllo").ToString())
Console.WriteLine("DllImport Unicode: " & Native.LenUnicode("héllo").ToString())
Console.WriteLine("DllImport Auto: " & Native.LenAuto("héllo").ToString())
Try
Console.WriteLine(LenAnsi("héllo"))
Catch ex As MarshalDirectiveException
Console.WriteLine("Declare Ansi: " & ex.Message)
End Try
End Sub
End Module
End Namespace
LenAnsi: System.String& marshal=VBByRefStr
LenMarshalled: System.String marshal=LPStr
LenUnicode: System.String marshal=LPWStr
LenAuto: System.String marshal=LPTStr
Declare LPStr: 6
Declare LPWStr: 1
Declare LPTStr: 1
DllImport none: 6
DllImport Ansi: 6
DllImport Unicode: 1
DllImport Auto: 6
Declare Ansi: Cannot marshal 'parameter #1': Invalid managed/unmanaged type combination (String parameters and return types must be paired with LPStr, LPWStr, or LPTStr).
Source: UnmanagedType
The result, in order of importance:
-
A plain
ByVal text As Stringin aDeclarebecomes aByRefstring withUnmanagedType.VBByRefStr(System.String&in the output). That marshalling type is the Visual Basic 6.0 convention that lets native code overwrite a string; the Learn page forUnmanagedTypemarks it obsolete ("may be unavailable in future releases") and says it is supported only for platform invoke. On this Linux container the call throwsMarshalDirectiveExceptionfor all ofAnsi,Unicode,Autoand the default. The Windows case was not run; Microsoft’s ownDeclaresamples use the plain form with Windows DLLs, so it may work there, but code that has to run on more than one operating system should not depend on it. -
Adding an explicit
<MarshalAs(...)>attribute to the parameter makes the compiler emit an ordinary by-valueStringwith that marshalling. That is the portable form, and everyDeclarein the rest of this page that takes a string uses it. -
With
<DllImport>there is no such conversion: aStringparameter is a by-value string, and theCharSetfield decides how it is encoded. -
The encoding is visible in the result.
strlen("héllo")counts bytes up to the first zero: 6 for UTF-8 (theétakes two bytes), but 1 for UTF-16, where the second byte ofhis already zero.Autofollows the platform: UTF-8 withDllImporton Unix (6 above), UTF-16 on Windows. TheDeclare Autowith an explicitLPTStrattribute printed 1, becauseLPTStris also UTF-16 here; avoidLPTStrand sayLPStr,LPUTF8StrorLPWStrexplicitly.
Source: Charsets and marshalling
CharSet |
Windows | Unix, .NET Core 3.0 and later | Typical native parameter |
|---|---|---|---|
|
system ANSI code page ( |
UTF-8 ( |
|
|
UTF-16 ( |
UTF-16 ( |
|
|
UTF-16 |
UTF-8 |
|
DllImport in Visual Basic
A method marked <DllImport> is declared like a normal method but must have an empty body. It can live in a class, structure or module;
in a class it is conventionally Friend Shared. Anything in the body is an error:
Option Strict On
Imports System.Runtime.InteropServices
Module Program
<DllImport("libc", EntryPoint:="getpid")>
Private Function GetPid() As Integer
Return 1
End Function
Sub Main()
End Sub
End Module
Program.vb(4,6): error BC31522: 'System.Runtime.InteropServices.DllImportAttribute' cannot be applied to a Sub, Function, or Operator with a non-empty body.
The fields that matter in practice:
| Field | Meaning |
|---|---|
|
the exported name when it differs from the Visual Basic name (the equivalent of |
|
string encoding and, with |
|
|
|
save |
|
|
|
|
|
behaviour for characters that do not exist in the target code page (Windows ANSI only) |
Finding the library
The name in Lib or DllImport is a logical name. The runtime adds the platform’s extension and, on Unix, the lib prefix, in a fixed order, and
loads the first file that exists. For "libc" on Linux it tries libc.so, liblibc.so, libc and liblibc in the application directory and the
system library path; for a name that already contains .so. (such as "libc.so.6") the exact name goes first. An absolute path is used as written.
Because the file for plain "libc" is not guaranteed to be usable on every distribution, portable code gives the versioned name or uses a resolver.
When nothing is found, or a function is not exported, the exceptions are:
Option Strict On
Imports System
Imports System.Reflection
Imports System.Runtime.InteropServices
Namespace E06
Friend Module Program
Private Declare Function Missing Lib "libshelf-does-not-exist" Alias "nothing" () As Integer
Private Declare Function NoSuchEntry Lib "libc" Alias "shelf_no_such_function" () As Integer
Private Declare Function GetPid Lib "shelf-libc" Alias "getpid" () As Integer
<UnmanagedFunctionPointer(CallingConvention.Cdecl)>
Private Delegate Function IntToInt(value As Integer) As Integer
Private Function Resolve(libraryName As String, assembly As Assembly, searchPath As DllImportSearchPath?) As IntPtr
If libraryName = "shelf-libc" Then
Console.WriteLine("resolver asked for '" & libraryName & "'")
Return NativeLibrary.Load("libc.so.6")
End If
Return IntPtr.Zero ' fall back to the default probing
End Function
Friend Sub Main()
Try
Missing()
Catch ex As DllNotFoundException
Console.WriteLine("DllNotFoundException: " & ex.Message.Substring(0, ex.Message.IndexOf(". In order", StringComparison.Ordinal) + 1))
End Try
Try
NoSuchEntry()
Catch ex As EntryPointNotFoundException
Console.WriteLine("EntryPointNotFoundException: " & ex.Message)
End Try
NativeLibrary.SetDllImportResolver(GetType(Program).Assembly, AddressOf Resolve)
Console.WriteLine("getpid via resolver > 0: " & (GetPid() > 0).ToString())
' no P/Invoke declaration at all: load, look up, wrap in a delegate
Dim libc As IntPtr = NativeLibrary.Load("libc.so.6")
Dim address As IntPtr = Nothing
Console.WriteLine("TryGetExport(abs): " & NativeLibrary.TryGetExport(libc, "abs", address).ToString() &
", TryGetExport(nope): " & NativeLibrary.TryGetExport(libc, "nope", address).ToString())
Dim abs As IntToInt = Marshal.GetDelegateForFunctionPointer(Of IntToInt)(NativeLibrary.GetExport(libc, "abs"))
Console.WriteLine("abs(-5) through a function pointer: " & abs(-5).ToString())
NativeLibrary.Free(libc)
End Sub
End Module
End Namespace
DllNotFoundException: Unable to load shared library 'libshelf-does-not-exist' or one of its dependencies.
EntryPointNotFoundException: Unable to find an entry point named 'shelf_no_such_function' in shared library 'libc'.
resolver asked for 'shelf-libc'
getpid via resolver > 0: True
TryGetExport(abs): True, TryGetExport(nope): False
abs(-5) through a function pointer: 5
Source: Native library loading
That program also shows the three tools Visual Basic has for taking control of loading:
-
NativeLibrary.SetDllImportResolver(assembly, callback)registers a callback that receives the logical name ("shelf-libc") and returns a library handle. ReturningIntPtr.Zerofalls back to the default probing. It can be set once per assembly. The callback must be a method with the signature(String, Assembly, DllImportSearchPath?) As IntPtr; the third parameter is a nullable enumeration, writtenDllImportSearchPath?. -
NativeLibrary.Load,TryLoad,GetExport,TryGetExportandFreework with handles directly. -
Marshal.GetDelegateForFunctionPointer(Of T)turns an export address into a callable delegate, with noDeclareat all. This is the only way to call a function whose name is chosen at run time.
The run-time failures are DllNotFoundException (library not found or one of its dependencies missing), EntryPointNotFoundException (wrong name or Alias),
BadImageFormatException (a 32-bit library in a 64-bit process, or the wrong CPU architecture) and MarshalDirectiveException (an impossible type combination, as above).
Types across the boundary
The compiler does not check a declaration against the library, so the type table is the most important part of the page. The column "VB" is the type to write in the declaration:
| C or Windows type | VB | Notes |
|---|---|---|
|
|
|
|
|
the same C type has different widths on Windows and Unix; check with |
|
|
|
|
|
|
|
|
|
|
|
16 bits on Windows, 32 bits on Linux and macOS |
|
|
marshalled as a 4-byte value by default, |
C99 |
|
a one-byte value |
|
|
|
|
|
pointer-sized; do not use |
|
|
see the ByVal String trap above |
|
|
the capacity is the buffer size |
|
|
initialise with |
|
structure |
the platform calling convention decides how it is passed; keep the layout identical |
function pointer |
a delegate type |
keep the delegate alive; see callbacks below |
|
|
or |
|
|
COM only |
Blittable types (integers, floating point, pointers, and structures made only of them) have the same representation on both sides, so they are
passed without copying. Boolean, Char and String are not blittable: they are converted at each call, which is slower and is the reason a
mismatch often shows up as a wrong value rather than a crash.
The Visual Basic 6.0 conversion table matters when you port an old Declare: VB6 Integer becomes Short, VB6 Long becomes Integer, As Any has no
equivalent (write one overload per type), a VB6 Type becomes a Structure with <StructLayout(LayoutKind.Sequential)>, and AddressOf callbacks need a delegate type.
Structures and fixed buffers
A structure passed to native code needs a known layout. <StructLayout(LayoutKind.Sequential)> lays the fields out in declaration order with the
platform’s natural alignment, Pack:=1 removes the padding and LayoutKind.Explicit with <FieldOffset> expresses unions. The next program uses
three native structures of Linux (timeval, utsname with six 65-byte character arrays, and the div_t that div returns by value), a
StringBuilder output buffer, and a function that returns a pointer:
Option Strict On
Imports System
Imports System.Runtime.InteropServices
Imports System.Text
Namespace E03
' struct timeval { time_t tv_sec; suseconds_t tv_usec; } on 64-bit Linux
<StructLayout(LayoutKind.Sequential)>
Friend Structure TimeVal
Public Seconds As Long
Public Microseconds As Long
End Structure
' struct utsname: six fixed 65-byte character arrays on Linux
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)>
Friend Structure UtsName
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=65)> Public SysName As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=65)> Public NodeName As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=65)> Public Release As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=65)> Public Version As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=65)> Public Machine As String
<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=65)> Public DomainName As String
End Structure
' div_t { int quot; int rem; } is returned by value
<StructLayout(LayoutKind.Sequential)>
Friend Structure DivResult
Public Quotient As Integer
Public Remainder As Integer
End Structure
Friend Module Program
Private Declare Function GetTimeOfDay Lib "libc" Alias "gettimeofday" (ByRef tv As TimeVal, tz As IntPtr) As Integer
Private Declare Function Uname Lib "libc" Alias "uname" (ByRef info As UtsName) As Integer
Private Declare Function Divide Lib "libc" Alias "div" (numerator As Integer, denominator As Integer) As DivResult
Private Declare Function GetCwd Lib "libc" Alias "getcwd" (buffer As StringBuilder, size As UIntPtr) As IntPtr
Private Declare Function GetEnv Lib "libc" Alias "getenv" (<MarshalAs(UnmanagedType.LPStr)> name As String) As IntPtr
Friend Sub Main()
Dim tv As TimeVal
Dim rc As Integer = GetTimeOfDay(tv, IntPtr.Zero)
Console.WriteLine("gettimeofday rc=" & rc.ToString() & ", seconds sane=" & (tv.Seconds > 1700000000L).ToString() &
", usec in range=" & (tv.Microseconds >= 0L AndAlso tv.Microseconds < 1000000L).ToString())
Dim info As UtsName = Nothing
rc = Uname(info)
Console.WriteLine("uname rc=" & rc.ToString() & ", sysname=" & info.SysName)
Console.WriteLine("Marshal.SizeOf(UtsName)=" & Marshal.SizeOf(Of UtsName)().ToString())
Dim d As DivResult = Divide(17, 5)
Console.WriteLine("div(17, 5): quotient=" & d.Quotient.ToString() & ", remainder=" & d.Remainder.ToString())
Dim buffer As New StringBuilder(260)
Dim p As IntPtr = GetCwd(buffer, New UIntPtr(CUInt(buffer.Capacity)))
Console.WriteLine("getcwd matches Directory.GetCurrentDirectory: " & (buffer.ToString() = System.IO.Directory.GetCurrentDirectory()).ToString() & ", pointer returned: " & (p <> IntPtr.Zero).ToString())
Dim value As IntPtr = GetEnv("SHELF_DEMO")
Console.WriteLine("getenv(SHELF_DEMO) = " & If(value = IntPtr.Zero, "(null)", Marshal.PtrToStringUTF8(value)))
End Sub
End Module
End Namespace
gettimeofday rc=0, seconds sane=True, usec in range=True
uname rc=0, sysname=Linux
Marshal.SizeOf(UtsName)=390
div(17, 5): quotient=3, remainder=2
getcwd matches Directory.GetCurrentDirectory: True, pointer returned: True
getenv(SHELF_DEMO) = (null)
Source: StructLayoutAttribute
Points to take from it:
-
The offsets and sizes follow the C declaration of the same platform. The structures here are 64-bit Linux layouts; on Windows or on a 32-bit system they would be different. Read the header for the platform you target.
-
A fixed-size character array in C is a
Stringfield with<MarshalAs(UnmanagedType.ByValTStr, SizeConst:=65)>. Visual Basic has no fixed-size array fields and nofixedstatement, so the attribute is the only way. -
A structure with reference-type fields (
String) passedByRefand left uninitialised triggers warning BC42108. It is an error here because the project treats warnings as errors:Program.vb(47,24): error BC42108: Variable 'info' is passed by reference before it has been assigned a value. A null reference exception could result at runtime. Make sure the structure or all the reference members are initialized before useThe fix used in the program is
Dim info As UtsName = Nothing. -
A
StringBuilderparameter is copied into a native buffer of itsCapacitybefore the call and back afterwards. Always construct it with the capacity you promise to the native function. -
getenvreturns a pointer into the process environment, which the caller must neither modify nor free. The program reads it withMarshal.PtrToStringUTF8only when it is not null.
Layout and unions can be checked without calling anything. This program prints the size and offsets the runtime computes, copies a structure through
unmanaged memory with memcpy, receives and frees a string allocated by C, and pins a managed array:
Option Strict On
Imports System
Imports System.Runtime.InteropServices
Namespace E07
<StructLayout(LayoutKind.Sequential, Pack:=1)>
Friend Structure CopyRecord
Public Id As Integer
Public Price As Double
Public Flag As Byte
End Structure
<StructLayout(LayoutKind.Explicit)>
Friend Structure IntOrFloat
<FieldOffset(0)> Public AsInteger As Integer
<FieldOffset(0)> Public AsSingle As Single
End Structure
Friend Module Program
Private Declare Function MemCopy Lib "libc" Alias "memcpy" (destination As IntPtr, source As IntPtr, count As UIntPtr) As IntPtr
Private Declare Function Duplicate Lib "libc" Alias "strdup" (<MarshalAs(UnmanagedType.LPUTF8Str)> text As String) As IntPtr
Private Declare Sub Release Lib "libc" Alias "free" (pointer As IntPtr)
Friend Sub Main()
Console.WriteLine("SizeOf CopyRecord (Pack:=1): " & Marshal.SizeOf(Of CopyRecord)().ToString() &
", OffsetOf Price: " & Marshal.OffsetOf(Of CopyRecord)("Price").ToString())
Console.WriteLine("SizeOf IntOrFloat: " & Marshal.SizeOf(Of IntOrFloat)().ToString())
Dim bits As New IntOrFloat
bits.AsSingle = 1.0F
Console.WriteLine("1.0F as bits: 0x" & bits.AsInteger.ToString("X8"))
' copy a structure through unmanaged memory
Dim source As New CopyRecord With {.Id = 7, .Price = 12.5, .Flag = 1}
Dim sourcePtr As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(Of CopyRecord)())
Dim targetPtr As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(Of CopyRecord)())
Try
Marshal.StructureToPtr(source, sourcePtr, False)
MemCopy(targetPtr, sourcePtr, New UIntPtr(CUInt(Marshal.SizeOf(Of CopyRecord)())))
Dim copy As CopyRecord = Marshal.PtrToStructure(Of CopyRecord)(targetPtr)
Console.WriteLine("copy: Id=" & copy.Id.ToString() & ", Price=" & copy.Price.ToString() & ", Flag=" & copy.Flag.ToString())
Finally
Marshal.FreeHGlobal(sourcePtr)
Marshal.FreeHGlobal(targetPtr)
End Try
' native allocation returned to us: copy it, then free it with the matching allocator
Dim native As IntPtr = Duplicate("Café Shelf")
Try
Console.WriteLine("strdup: " & Marshal.PtrToStringUTF8(native) & " (" & Marshal.PtrToStringUTF8(native).Length.ToString() & " chars)")
Finally
Release(native)
End Try
' pin a managed array so native code can see a stable address
Dim numbers As Integer() = {10, 20, 30}
Dim handle As GCHandle = GCHandle.Alloc(numbers, GCHandleType.Pinned)
Try
Dim address As IntPtr = handle.AddrOfPinnedObject()
Console.WriteLine("element 2 read through the pointer: " & Marshal.ReadInt32(address, 2 * Marshal.SizeOf(Of Integer)()).ToString())
Marshal.WriteInt32(address, 0, 99)
Finally
handle.Free()
End Try
Console.WriteLine("numbers(0) after the native write: " & numbers(0).ToString())
End Sub
End Module
End Namespace
SizeOf CopyRecord (Pack:=1): 13, OffsetOf Price: 4
SizeOf IntOrFloat: 4
1.0F as bits: 0x3F800000
copy: Id=7, Price=12.5, Flag=1
strdup: Café Shelf (10 chars)
element 2 read through the pointer: 30
numbers(0) after the native write: 99
Source: Marshal class
Marshal.AllocHGlobal and FreeHGlobal are a pair; so are StringToCoTaskMemUTF8 and FreeCoTaskMem; memory that C allocated (strdup) must go back to C’s free,
not to FreeHGlobal. Passing the wrong deallocator corrupts the heap on Linux and is a silent leak or a crash on Windows. Pinning with GCHandle is only
needed when native code keeps the address after the call returns; for the duration of a call the runtime pins arrays and structures passed by reference itself.
Callbacks
A native function that takes a function pointer receives a Visual Basic delegate. The delegate type must match the native signature and should
carry <UnmanagedFunctionPointer(CallingConvention.Cdecl)> when the callee is not stdcall. qsort from the C library is the classic example:
Option Strict On
Imports System
Imports System.Runtime.InteropServices
Namespace E04
Friend Module Program
' int (*compar)(const void *, const void *)
<UnmanagedFunctionPointer(CallingConvention.Cdecl)>
Friend Delegate Function CompareCallback(left As IntPtr, right As IntPtr) As Integer
Private Declare Sub QuickSort Lib "libc" Alias "qsort" (<[In], Out> items As Integer(), count As UIntPtr, size As UIntPtr, compare As CompareCallback)
Private callsMade As Integer
' the comparer reads the two elements straight from native memory
Private Function Descending(left As IntPtr, right As IntPtr) As Integer
callsMade += 1
Dim a As Integer = Marshal.ReadInt32(left)
Dim b As Integer = Marshal.ReadInt32(right)
Return b.CompareTo(a)
End Function
Friend Sub Main()
Dim loans As Integer() = {14, 3, 9, 27, 1, 8}
Dim compare As New CompareCallback(AddressOf Descending)
QuickSort(loans, New UIntPtr(CUInt(loans.Length)), New UIntPtr(CUInt(Marshal.SizeOf(Of Integer)())), compare)
GC.KeepAlive(compare) ' the delegate must stay alive while native code can call it
Console.WriteLine(String.Join(", ", loans))
Console.WriteLine("comparer called: " & (callsMade > 0).ToString())
End Sub
End Module
End Namespace
27, 14, 9, 8, 3, 1
comparer called: True
Source: Callbacks in P/Invoke
Rules for callbacks:
-
AddressOf Descendingcreates the delegate; Visual Basic converts the delegate type implicitly in a call, but writingNew CompareCallback(AddressOf Descending)makes the lifetime visible. UnderOption Strict Onthe method must match the delegate’s parameter types exactly (BC36663 for a method with fewer parameters, which lambdas can do andAddressOfcannot). -
The runtime keeps a delegate alive for the duration of a call, but not after it. If native code stores the pointer (a window procedure, a registered callback), keep the delegate in a field or
GC.KeepAliveit as long as the native side can call it; otherwise a collection leads to a crash that is hard to trace. -
An exception that escapes a callback unwinds through native frames and terminates the process. Catch inside the callback and signal failure through a return value.
-
The callback runs on the thread that called the native function unless the library calls it from its own threads, in which case no
SynchronizationContextis available and UI objects must not be touched directly.
Handles and error codes
An integer or IntPtr that identifies an operating-system resource should be wrapped in a SafeHandle, so that it is released once, even when an exception
or a thread abort interrupts the program. A subclass supplies ReleaseHandle; SafeHandleMinusOneIsInvalid and SafeHandleZeroOrMinusOneIsInvalid exist for the two common invalid values. Error codes
are read after the call with Marshal.GetLastPInvokeError(), but only when SetLastError is on. This program wraps a Linux file descriptor, reads a file through
read, shows the handle being closed and, last, shows a pitfall of declaring the wrong return type:
Option Strict On
Imports System
Imports System.ComponentModel
Imports System.IO
Imports System.Runtime.InteropServices
Imports System.Text
Imports Microsoft.Win32.SafeHandles
Namespace E05
' A SafeHandle owns a native file descriptor and closes it exactly once
Friend NotInheritable Class FileDescriptor
Inherits SafeHandleMinusOneIsInvalid
Public Sub New()
MyBase.New(True)
End Sub
Friend Sub New(descriptor As Integer)
MyBase.New(True)
SetHandle(New IntPtr(descriptor))
End Sub
Protected Overrides Function ReleaseHandle() As Boolean
Return Native.Close(handle) = 0
End Function
End Class
Friend NotInheritable Class Native
Private Sub New()
End Sub
' open() returns a 32-bit int, so the SafeHandle return type is wrong here (see the output)
<DllImport("libc", EntryPoint:="open", SetLastError:=True)>
Friend Shared Function OpenAsHandle(<MarshalAs(UnmanagedType.LPStr)> path As String, flags As Integer) As FileDescriptor
End Function
<DllImport("libc", EntryPoint:="open", SetLastError:=True)>
Friend Shared Function OpenRaw(<MarshalAs(UnmanagedType.LPStr)> path As String, flags As Integer) As Integer
End Function
<DllImport("libc", EntryPoint:="read", SetLastError:=True)>
Friend Shared Function Read(fd As FileDescriptor, buffer As Byte(), count As UIntPtr) As IntPtr
End Function
<DllImport("libc", EntryPoint:="close")>
Friend Shared Function Close(fd As IntPtr) As Integer
End Function
Friend Shared Function Open(path As String) As FileDescriptor
Dim raw As Integer = OpenRaw(path, 0)
If raw < 0 Then
Throw New Win32Exception(Marshal.GetLastPInvokeError())
End If
Return New FileDescriptor(raw)
End Function
End Class
Friend Module Program
Friend Sub Main()
Dim path As String = System.IO.Path.GetTempFileName()
File.WriteAllText(path, "ISBN 978-0-13-468599-1" & vbLf)
Dim fd As FileDescriptor = Native.Open(path)
Console.WriteLine("valid: " & (Not fd.IsInvalid).ToString() & ", closed: " & fd.IsClosed.ToString())
Dim buffer(63) As Byte
Dim count As Integer = CInt(Native.Read(fd, buffer, New UIntPtr(CUInt(buffer.Length))))
Console.Write(Encoding.ASCII.GetString(buffer, 0, count))
fd.Dispose()
Console.WriteLine("after Dispose, closed: " & fd.IsClosed.ToString())
Try
Native.Read(fd, buffer, New UIntPtr(CUInt(buffer.Length)))
Catch ex As ObjectDisposedException
Console.WriteLine("read on a closed handle: " & ex.GetType().Name)
End Try
' SetLastError captures errno right after the call
Dim missing As String = path & ".missing"
Dim naive As FileDescriptor = Native.OpenAsHandle(missing, 0)
Console.WriteLine("SafeHandle return type, IsInvalid after a failed open: " & naive.IsInvalid.ToString())
naive.Dispose()
Try
Native.Open(missing)
Catch ex As Win32Exception
Console.WriteLine("raw Integer return type: errno=" & ex.NativeErrorCode.ToString() & ", message=" & ex.Message)
End Try
File.Delete(path)
End Sub
End Module
End Namespace
valid: True, closed: False
ISBN 978-0-13-468599-1
after Dispose, closed: True
read on a closed handle: ObjectDisposedException
SafeHandle return type, IsInvalid after a failed open: False
raw Integer return type: errno=2, message=No such file or directory
Source: SafeHandle
The last two lines are the instructive ones. open returns a 32-bit int (-1 on failure), but a SafeHandle return type makes the runtime read a pointer-sized value from the
return register, and the upper half of that register is not defined by the C ABI. Here it held zero, so the handle was 4294967295, not -1, and IsInvalid said False for a failed call. The program
shows the robust pattern: declare the return type as Integer, test it, and construct the SafeHandle yourself. The error code comes out as errno 2 ("No such file or directory") through Win32Exception, which
formats native error codes on every platform. SafeHandle return types are safe for APIs that return a real pointer-sized handle, such as CreateFile on Windows.
Calls that exist only on Windows
Declare and DllImport compile against any library name, so a program for Windows APIs builds on every operating system and fails at the first call elsewhere. The next two programs call
kernel32 and user32. They were built with EnableWindowsTargeting and are built, not run: no Windows machine was available.
Built, not run. Common Windows functions with Declare, the Auto modifier and an out buffer:
Option Strict On
Imports System
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Text
Namespace W1
Friend Module Program
' Auto: the runtime looks for MessageBox, then MessageBoxW
Private Declare Auto Function MessageBox Lib "user32" (hWnd As IntPtr, text As String, caption As String, type As UInteger) As Integer
' The same API with explicit marshalling, which does not depend on VBByRefStr
Private Declare Function MessageBoxW Lib "user32" (hWnd As IntPtr,
<MarshalAs(UnmanagedType.LPWStr)> text As String,
<MarshalAs(UnmanagedType.LPWStr)> caption As String, type As UInteger) As Integer
Private Declare Auto Function GetComputerName Lib "kernel32" (buffer As StringBuilder, ByRef size As UInteger) As Boolean
Private Declare Function GetTickCount64 Lib "kernel32" () As ULong
Private Declare Function GetFileAttributesW Lib "kernel32" (<MarshalAs(UnmanagedType.LPWStr)> path As String) As UInteger
Private Const InvalidFileAttributes As UInteger = &HFFFFFFFFUI
Private Const MbOk As UInteger = 0UI
Private Const MbIconInformation As UInteger = &H40UI
Friend Sub Main()
Dim name As New StringBuilder(256)
Dim size As UInteger = CUInt(name.Capacity)
If GetComputerName(name, size) Then
Console.WriteLine("Computer: " & name.ToString() & " (" & size.ToString() & " characters)")
End If
Console.WriteLine("Uptime: " & TimeSpan.FromMilliseconds(CDbl(GetTickCount64())).ToString("d\.hh\:mm\:ss"))
If GetFileAttributesW("C:\Shelf\missing.txt") = InvalidFileAttributes Then
Dim code As Integer = Marshal.GetLastPInvokeError()
Console.WriteLine("GetFileAttributes failed: " & code.ToString() & " " & New Win32Exception(code).Message)
End If
MessageBox(IntPtr.Zero, "The shelf has 2 books.", "Shelf", MbOk Or MbIconInformation)
MessageBoxW(IntPtr.Zero, "Same call, explicit marshalling.", "Shelf", MbOk)
End Sub
End Module
End Namespace
Built, not run. A structure, a callback into EnumWindows and a SafeFileHandle return type:
Option Strict On
Imports System
Imports System.Collections.Generic
Imports System.Runtime.InteropServices
Imports System.Text
Imports Microsoft.Win32.SafeHandles
Namespace W2
' SYSTEM_INFO: the first DWORD is a union, read here as the architecture and reserved words
<StructLayout(LayoutKind.Sequential)>
Friend Structure SystemInfo
Public ProcessorArchitecture As UShort
Public Reserved As UShort
Public PageSize As UInteger
Public MinimumApplicationAddress As IntPtr
Public MaximumApplicationAddress As IntPtr
Public ActiveProcessorMask As UIntPtr
Public NumberOfProcessors As UInteger
Public ProcessorType As UInteger
Public AllocationGranularity As UInteger
Public ProcessorLevel As UShort
Public ProcessorRevision As UShort
End Structure
' BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
<UnmanagedFunctionPointer(CallingConvention.Winapi)>
Friend Delegate Function EnumWindowsCallback(hWnd As IntPtr, lParam As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
Friend Module Program
Private Declare Sub GetNativeSystemInfo Lib "kernel32" (ByRef info As SystemInfo)
Private Declare Function EnumWindows Lib "user32" (callback As EnumWindowsCallback, lParam As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
Private Declare Auto Function GetWindowText Lib "user32" (hWnd As IntPtr, buffer As StringBuilder, maxCount As Integer) As Integer
Private Declare Function IsWindowVisible Lib "user32" (hWnd As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
<DllImport("kernel32", EntryPoint:="CreateFileW", CharSet:=CharSet.Unicode, ExactSpelling:=True, SetLastError:=True)>
Private Function CreateFile(path As String, access As UInteger, share As UInteger, security As IntPtr,
disposition As UInteger, flags As UInteger, template As IntPtr) As SafeFileHandle
End Function
Private Const GenericRead As UInteger = &H80000000UI
Private Const FileShareRead As UInteger = 1UI
Private Const OpenExisting As UInteger = 3UI
Friend Sub Main()
Dim info As SystemInfo = Nothing
GetNativeSystemInfo(info)
Console.WriteLine("Processors: " & info.NumberOfProcessors.ToString() & ", page size: " & info.PageSize.ToString())
Dim titles As New List(Of String)
Dim callback As New EnumWindowsCallback(
Function(hWnd As IntPtr, lParam As IntPtr) As Boolean
If IsWindowVisible(hWnd) Then
Dim text As New StringBuilder(256)
If GetWindowText(hWnd, text, text.Capacity) > 0 Then titles.Add(text.ToString())
End If
Return True
End Function)
EnumWindows(callback, IntPtr.Zero)
GC.KeepAlive(callback)
Console.WriteLine("Visible top-level windows with a title: " & titles.Count.ToString())
Using file As SafeFileHandle = CreateFile("C:\Windows\win.ini", GenericRead, FileShareRead, IntPtr.Zero, OpenExisting, 0UI, IntPtr.Zero)
Console.WriteLine("win.ini handle valid: " & (Not file.IsInvalid).ToString())
End Using
End Sub
End Module
End Namespace
What each part is meant to show:
-
Declare Autolooks the function up asMessageBox, and if that name is not exported, asMessageBoxW(the Declare reference describes this order for a Unicode platform), and marshalsStringas UTF-16. Windows exports bothMessageBoxAandMessageBoxWand no plainMessageBox, which is whyAutois the traditional choice for the Windows API. -
The second form,
MessageBoxWwith explicitLPWStrparameters, does not depend on name probing or onVBByRefStr. That is the form to prefer in new code, together withExactSpelling. -
EnumWindowsandIsWindowVisiblereturn a Win32BOOL, which is four bytes;<MarshalAs(UnmanagedType.Bool)>on the return type states that explicitly (it is also the default forBoolean). A C99boolorBOOLEANwould needUnmanagedType.U1. -
GetFileAttributesWfails with the value&HFFFFFFFF; the error code read withMarshal.GetLastPInvokeError()is only meaningful becauseDeclareturnedSetLastErroron. -
Unsigned parameters are declared
UInteger,UShortandULong. Constants such as&H80000000UIneed theUIsuffix, otherwise the literal is a negativeInteger.
Running either program on Linux shows what a non-Windows platform does with a Windows library name:
Unhandled exception. System.DllNotFoundException: Unable to load shared library 'kernel32' or one of its dependencies. In order to help diagnose loading problems, consider using a tool like strace. If you're using glibc, consider setting the LD_DEBUG environment variable:
/usr/share/dotnet/shared/Microsoft.NETCore.App/10.0.13/kernel32.so: cannot open shared object file: No such file or directory
/w/bin/Debug/net10.0/kernel32.so: cannot open shared object file: No such file or directory
/usr/share/dotnet/shared/Microsoft.NETCore.App/10.0.13/libkernel32.so: cannot open shared object file: No such file or directory
/w/bin/Debug/net10.0/libkernel32.so: cannot open shared object file: No such file or directory
This is the same probing as before: kernel32.so, libkernel32.so, kernel32, libkernel32, each in the framework folder and the application folder.
Keeping platform-specific calls out of other platforms
<SupportedOSPlatform("windows")>, and the analyzer rule CA1416 warns about any call site that is reachable on other platforms. You can annotate your own declarationsthe same way and use OperatingSystem.IsWindows() and its siblings as guards. The program below declares one native function per platform, guards the calls, and compares the result with the portable
Environment.ProcessId:
Option Strict On
Imports System
Imports System.Runtime.Versioning
Namespace E08
Friend Module Program
<SupportedOSPlatform("windows")>
Private Declare Function GetCurrentProcessId Lib "kernel32" () As UInteger
<SupportedOSPlatform("linux")>
<SupportedOSPlatform("macos")>
Private Declare Function GetPid Lib "libc" Alias "getpid" () As Integer
Private Function ProcessIdFromNative() As Long
If OperatingSystem.IsWindows() Then
Return GetCurrentProcessId()
End If
If OperatingSystem.IsLinux() OrElse OperatingSystem.IsMacOS() Then
Return GetPid()
End If
Throw New PlatformNotSupportedException("No native process id binding for " & Runtime.InteropServices.RuntimeInformation.OSDescription)
End Function
Friend Sub Main()
Console.WriteLine("native pid equals Environment.ProcessId: " & (ProcessIdFromNative() = Environment.ProcessId).ToString())
End Sub
End Module
End Namespace
native pid equals Environment.ProcessId: True
Source: Platform compatibility analyzer
Removing the guard from a call to an annotated declaration produces the warning below (shown with warnings not treated as errors; in these projects it fails the build):
Program.vb(8,27): warning CA1416: This call site is reachable on all platforms. 'Private Declare Ansi Function GetCurrentProcessId Lib "kernel32" () As UInteger' is only supported on: 'windows'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1416)
Prefer the managed API whenever one exists: Environment.ProcessId, System.IO, System.Diagnostics.Process and Microsoft.Win32.Registry cover most of what older code used Declare for. Reach for P/Invoke
when no managed equivalent exists or when you are talking to your own native library.
LibraryImport and function pointers
C# 11 and .NET 7 added [LibraryImport], a source generator that writes the marshalling code at compile time, which allows trimming and native ahead-of-time compilation. The
generator is part of the C# compiler pipeline; Visual Basic has no counterpart. The attribute exists in the base class library, so the code below compiles, but nothing completes it:
Option Strict On
Imports System.Runtime.InteropServices
Module Program
<LibraryImport("libc", EntryPoint:="getpid")>
Private Partial Function GetPid() As Integer
End Function
Sub Main()
End Sub
End Module
Program.vb(5,30): error BC31437: 'GetPid' cannot be declared 'Partial' because partial methods must be Subs.
With a Sub the declaration compiles, because Visual Basic partial methods may lack an implementation, and the call is removed without a diagnostic:
Option Strict On
Imports System
Imports System.Runtime.InteropServices
Module Program
<LibraryImport("libc", EntryPoint:="exit")>
Private Partial Sub NativeExit(code As Integer)
End Sub
Sub Main()
Console.WriteLine("before")
NativeExit(3)
Console.WriteLine("after: the call above was silently removed")
End Sub
End Module
before
after: the call above was silently removed
The program printed both lines and never exited with code 3. For the same reason [UnmanagedCallersOnly] (a method that native code can call directly) is rejected:
Program.vb(4,6): error BC37316: 'UnmanagedCallersOnly' attribute is not supported.
Source: P/Invoke source generation
The practical consequences: in Visual Basic you use Declare or DllImport with run-time marshalling, you pass callbacks as delegates, you call exports by address with Marshal.GetDelegateForFunctionPointer, and if you need a
source-generated or function-pointer binding you put it in a small C# project of the same solution and reference it (see
Interop with C# and Other .NET Languages).
Compiler and runtime messages
Each declaration-level problem was built in its own program, because one declaration error hides the others. Messages are as printed by the SDK:
| Code | Cause | Message |
|---|---|---|
BC31522 |
|
|
BC30244 |
|
|
BC30218 |
|
|
BC30603 |
|
|
BC30001 |
|
|
BC30289 |
|
|
BC32075 |
|
|
BC30210, BC30211 |
missing |
|
BC30512 |
argument of the wrong type, for example |
|
BC31437 |
|
|
BC37316 |
|
|
BC42108 |
uninitialised structure passed |
|
BC30183 |
a variable named |
|
CA1416 |
call to a platform-restricted declaration |
|
The run-time exceptions seen on this page: MarshalDirectiveException (its message starts "Cannot marshal 'parameter #1': Invalid managed/unmanaged type combination"), DllNotFoundException, EntryPointNotFoundException and ObjectDisposedException (a disposed SafeHandle passed to a call).
Visual Basic versus C#
| Topic | Visual Basic | C# |
|---|---|---|
Declaration |
|
|
Default string handling |
|
|
|
always on for |
off unless set |
Source-generated P/Invoke |
not supported (BC31437, silent removal for a |
|
Function pointers |
none; delegates and |
|
Pointers and |
none; |
|
Output parameters |
|
|
Fixed-size buffers in a struct |
|
|
|
same rules |
same rules |
Native AOT export of managed methods |
not available |
|
Native Interop (C#) gives the C# column in detail, and Unsafe Code, Spans and Performance shows the pointer-based style that Visual Basic does not have.
Older material you may meet
-
Visual Basic 6.0 and early .NET code declares
ByVal x As StringwithAs Any, usesLongfor a 32-bit value, and relies onGetLastError(). On .NET useMarshal.GetLastPInvokeError(); a direct call toGetLastErroris not reliable because the runtime may call other native functions in between. -
Books and blog posts about .NET Framework describe the Windows-only
Declarebehaviour, such as the lookup ofAandWnames andVBByRefStr, as if it were universal. On .NET 10 the same program must run on more than one operating system, and an explicitMarshalAsremoves the dependency. -
Parameter names in a
Declareare documentation for Visual Basic callers only; the native library never sees them.abscalls its parameterjin the C library, and the declarations above name itvalue, which is correct. Named arguments in a call use the Visual Basic name. -
DeclarewithAlias "#nn"selects a Windows export by ordinal; it has no meaning on Linux or macOS.
Pitfalls checklist
-
Never write a bare
ByVal text As Stringin aDeclarethat has to run on Linux or macOS; add<MarshalAs(...)>or use<DllImport>. -
Match integer widths to the target platform:
longis 32 bits on Windows and 64 bits on Linux and macOS. -
Use
IntPtrandUIntPtrfor handles, pointers andsize_t. -
Initialise structures with
Newor= Nothingbefore passing themByRef, and size everyStringBuilder. -
Return
Integerfrom an API that returns a Cint, and wrap it in aSafeHandleyourself. -
Free memory with the allocator that produced it.
-
Keep the delegate of a callback alive for as long as native code can call it, and never let an exception leave it.
-
Read the error code immediately after the call, and only when
SetLastErroris on. -
Wrap every platform-specific declaration in
OperatingSystem.Is...()guards and<SupportedOSPlatform>. -
Prefer a managed API; prefer a small C# project for
LibraryImportor function pointers.
References
-
Platform Invoke (P/Invoke), Charsets and marshalling, Type marshalling and Native interoperability best practices
-
DllImportAttribute, StructLayoutAttribute, UnmanagedType, Marshal and Marshal.GetLastPInvokeError
-
Platform compatibility analyzer, SupportedOSPlatformAttribute and CA1416
-
MessageBoxW function (Windows API documentation)
-
COM Interop and Office Automation, Interop with C# and Other .NET Languages and Object Lifetime and Disposal in this section