P/Invoke declaration lives outside a NativeMethods class
ID |
vbnet.maintainability.move_p_invokes_to_native_methods_class |
Severity |
low |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Interop |
Language |
VB.NET |
Description
Reports a P/Invoke declaration - a <DllImport> Function or Sub, or a legacy Declare
statement - declared inside a type whose name is not NativeMethods, SafeNativeMethods or
UnsafeNativeMethods. The convention these three names encode is what the check enforces:
native declarations belong in a dedicated container, not alongside the business logic that
calls them. Declarations already inside one of the three are not reported, whatever their
accessibility.
Rationale
Where a native declaration lives determines how much of the code base has to be reviewed as
unmanaged interop. A P/Invoke sitting among ordinary members hides two properties of that
member from the reader: it can be called with no security demand once the containing type is
trusted, and its correctness depends on marshalling that no compiler checks. Reviewers who
audit a type for one reason do not usually audit it for the other, so a signature edited to add
a parameter, or a CharSet left at its default, passes review in a file nobody was reviewing
as interop code. Scattering also multiplies the declarations themselves: the same
CreateFile or CancelJob gets declared in three classes with three slightly different
signatures, and only one of them is correct on 64-bit. The three conventional names carry
meaning that reviewers and analysis tools rely on - SafeNativeMethods for calls that are
harmless to expose, UnsafeNativeMethods for calls that must not be, NativeMethods for
everything that has not been classified - so grouping the declarations is also what makes it
possible to state, in one place, which native calls the assembly makes at all.
The following code illustrates the pattern detected by this rule:
Public Sub Cancel(ByVal jobId As Integer)
CancelJob(handle, jobId)
End Sub
' FLAGGED: P/Invoke declaration lives outside a NativeMethods class
<DllImport("winspool.drv", SetLastError:=True)>
Private Shared Function CancelJob(printer As IntPtr, job As Integer) As Boolean
End Function
Remediation
Move the declaration into a Friend NotInheritable Class NativeMethods with a private
constructor, alongside the other native declarations of the assembly, and call it from a
managed wrapper method that validates arguments and translates failures into exceptions. Use
SafeNativeMethods for entry points that are safe for any caller to invoke and
UnsafeNativeMethods for those that are not, so the distinction is visible from the call site.
Keep one declaration per entry point, shared by every caller.
' Before: a native declaration among the business members of PrintSpooler
Public Class PrintSpooler
<DllImport("winspool.drv", SetLastError:=True)>
Private Shared Function CancelJob(printer As IntPtr, job As Integer) As Boolean
End Function
End Class
' After
Friend NotInheritable Class NativeMethods
Private Sub New()
End Sub
<DllImport("winspool.drv", SetLastError:=True)>
Friend Shared Function CancelJob(printer As IntPtr, job As Integer) As Boolean
End Function
End Class