PInvokeHelper.psm1

# Copyright: (c) 2019, Jordan Borean (@jborean93) <jborean93@gmail.com>
# MIT License (see LICENSE or https://opensource.org/licenses/MIT)

Function Set-MarshalAsAttribute {
    <#
    .SYNOPSIS
    Adds the MarshalAs attribute.
 
    .DESCRIPTION
    Add the [MarshalAs()] attribute to a builder object.
 
    .PARAMETER Builder
    The builder object to add the MarshalAs attribute to.
 
    .PARAMETER Type
    The UnmanagedType value to set on the MarshalAs attribute.
 
    .PARAMETER SizeConst
    The SizeConst value to set on the MarshalAs attribute.
 
    .PARAMETER ArraySubType
    The ArraySubType value to set on the MarshalAs attribute.
 
    .EXAMPLE
    Set-MarshalAsAttribute -Builder $method_builder -Type LPWStr -SizeConst 8
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="This does make a system change but localised to the passed in builder")]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [Object]
        $Builder,

        [Parameter(Mandatory=$true)]
        [System.Runtime.InteropServices.UnmanagedType]
        $Type,

        [System.Int32]
        $SizeConst,

        [System.Runtime.InteropServices.UnmanagedType]
        $ArraySubType
    )

    $ctor_info = [System.Runtime.InteropServices.MarshalAsAttribute].GetConstructor(
        [System.Runtime.InteropServices.UnmanagedType]
    )
    $no_params = @{
        TypeName = 'System.Reflection.Emit.CustomAttributeBuilder'
        ArgumentList = [System.Collections.Generic.List`1[Object]]@($ctor_info, $Type)
    }

    $field_array = [System.Collections.Generic.List`1[System.Reflection.FieldInfo]]@()
    $field_values = [System.Collections.Generic.List`1[Object]]@()
    if ($null -ne $SizeConst) {
        $field_array.Add([System.Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst'))
        $field_values.Add($SizeConst)
    }
    if ($null -ne $ArraySubType) {
        $field_array.Add([System.Runtime.InteropServices.MarshalAsAttribute].GetField('ArraySubType'))
        $field_values.Add($ArraySubType)
    }
    $no_params.ArgumentList.Add($field_array.ToArray())
    $no_params.ArgumentList.Add($field_values.ToArray())

    $attribute_builder = New-Object @no_params
    $Builder.SetCustomAttribute($attribute_builder)
}

Function Get-Win32ErrorMessage {
    <#
    .SYNOPSIS
    Gets the Win32 Error message.
 
    .DESCRIPTION
    Gets the Win32 Error message based on error code passed in
 
    .PARAMETER ErrorCode
    The error code to convert. \When calling a PInvoke function this can be retrieved with
        [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
 
    This should be done inline, e.g.
        [PInvoke]::Function(); $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
 
    Failure to do this in line may result in the wrong error code being retrieved.
 
    .OUTPUTS
    System.String. The Win32 error message for the code specified.
 
    .EXAMPLE
    $res = [PInvoke]::CreatePipe(
        [Ref]$ReadHandle,
        [Ref]$WriteHandle,
        0,
        0
    ); $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
 
    if (-not $res) {
        $msg = Get-Win32ErrorMessage -ErrorCode $err_code
        throw $msg
    }
    #>

    [OutputType([System.String])]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [System.Int32]
        $ErrorCode
    )

    $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
    ('{0} (Win32 ErrorCode {1} - 0x{1:X8})' -f $exp.Message, $ErrorCode)
}

Function Import-Enum {
    <#
    .SYNOPSIS
    Defines an enum.
 
    .DESCRIPTION
    Defines an enum in the module builder specified.
 
    .PARAMETER ModuleBuilder
    The ModuleBuilder object to define the enum in.
 
    .PARAMETER Name
    The name of the enum to define.
 
    .PARAMETER Type
    The enum base type.
 
    .PARAMETER Values
    A hashtable of enum key/values to define.
 
    .PARAMETER Flags
    Whether the enum has the [Flags] attribute applied.
 
    .EXAMPLE
    $module_builder = New-DynamicModule -Name PInvokeHelper
 
    Defines
        [Flags]
        public LogonFlags : uint
        {
            WithoutProfile = 0,
            WithProfile = 1,
            NetCredentialsOnly = 2
        }
 
    $enum = @{
        ModuleBuilder = $module_builder
        Name = 'LogonFlags'
        Type = ([System.UInt32])
        Flags = $true
        Values = @{
            WithoutProfile = 0
            WithProfile = 1
            NetCredentialsOnly = 2
        }
    }
    Import-Enum @enum
 
    Defines
        public TokenInformationClass : int
        {
            User = 1,
            Groups = 2,
            Privileges = 3
        }
 
    $enum = @{
        ModuleBuilder = $module_builder
        Name = 'TokenInformationClass'
        Type = ([System.Int32])
        Values = @{
            User = 1
            Groups = 2
            Privileges = 3
        }
    }
    Import-Enum @enum
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [System.Reflection.Emit.ModuleBuilder]
        $ModuleBuilder,

        [Parameter(Mandatory=$true)]
        [System.String]
        $Name,

        [Parameter(Mandatory=$true)]
        [Type]
        $Type,

        [Parameter(Mandatory=$true)]
        [System.Collections.IDictionary]
        $Values,

        [Switch]
        $Flags
    )

    $enum_builder = $ModuleBuilder.DefineEnum($Name, 'Public', $Type)

    # Add the Flags attribute if required
    if ($Flags) {
        $ctor_info = [System.FlagsAttribute].GetConstructor([Type]::EmptyTypes)
        $attribute_builder = New-Object -TypeName System.Reflection.Emit.CustomAttributeBuilder -ArgumentList @(
            $ctor_info,
            @()
        )
        $enum_builder.SetCustomAttribute($attribute_builder)
    }

    foreach ($kvp in $Values.GetEnumerator()) {
        $value = $kvp.Value -as $Type
        $enum_builder.DefineLiteral($kvp.Key, $value) > $null
    }
    $enum_builder.CreateType() > $null
}

Function Import-PInvokeMethod {
    <#
    .SYNOPSIS
    Defines a Win32 function as a PInvoke method.
 
    .DESCRIPTION
    Defines a Win32 function as a PInvoke method and control some of the marshaling techniques used for that function.
 
    .PARAMETER TypeBuilder
    A TypeBuilder that represents the class to define the static methods in.
 
    .PARAMETER DllName
    The name of the Dll that contains the function to import.
 
    .PARAMETER Name
    The name of the Win32 function to import.
 
    .PARAMETER ReturnType
    The type that is returned by the function. Can also be a Hashtable with the following keys;
 
        Type: The type of the return value of the function.
        MarshalAs: An optional hash that contains the parameter for Set-MarshalAsAttribute.
            Type: Sets the [MarshalAs(UnmanagedType.)] value for the parameter.
            SizeConst: Optional SizeConst value for MarshalAs.
            ArraySubType: Optional ArraySubType value for MarshalAs.
 
    .PARAMETER ParameterTypes
    A list of types of the functions parameters. Each parameter is represented by a [Type] value for a simple
    parameter type but can also be a hashtable with the following keys;
 
        Type: Must be set to the type of the parameter
        Ref: Whether it is a [Ref] type, e.g. is ref or has an out parameter in .NET.
        MarshalAs: An optional hash that contains the parameter for Set-MarshalAsAttribute.
            Type: Sets the [MarshalAs(UnmanagedType.)] value for the parameter.
            SizeConst: Optional SizeConst value for MarshalAs.
            ArraySubType: Optional ArraySubType value for MarshalAs.
 
    .PARAMETER SetLastError
    Sets the 'SetLastError=true' field on the DllImport attribute of the function.
 
    .PARAMETER CharSet
    Sets the 'CharSet' field on the DllImport attribute of the function.
 
    .PARAMETER PassThru
    Output the MethodBuilder of the defined function.
 
    .OUTPUTS
    None, System.Reflection.Emit.MethodBuilder
    This cmdlet generates a System.Reflection.Emit.MethodInfo if you specify -PassThru, Otherwise this cmdlet does not
    return any output.
 
    .EXAMPLE
    $module_builder = New-DynamicModule -Name PInvokeHelper
    $type_builder = $module_buidler.DefineType('PInvokeHelper.NativeMethods', 'Public, Class')
 
    $function_definition = @{
        DllName = 'Kernel32.dll'
        Name = 'CreateProcessW'
        ReturnType = ([System.Boolean])
        ParameterTypes = @(
            @{
                Type = ([System.String])
                MarshalAs = @{
                    Type = [System.Runtime.InteropServices.UnmanagedType]::LPWStr
                }
            },
            [System.Text.StringBuilder],
            [System.IntPtr],
            [System.IntPtr],
            [System.Boolean],
            [PSLimitedProcess.ProcessCreationFlags],
            [System.IntPtr],
            @{
                Type = ([System.String])
                MarshalAs = @{
                    Type = [System.Runtime.InteropServices.UnmanagedType]::LPWStr
                }
            },
            @{ Ref = $true; Type = [PSLimitedProcess.STARTUPINFOEX] },
            @{ Ref = $true; Type = [PSLimitedProcess.PROCESS_INFORMATION] }
        )
        SetLastError = $true
    }
    Import-PInvokeMethod -TypeBuilder $type_builder @function_definition
 
    # Call once all functions have been defined in the type/class
    $type_builder.CreateType() > $null
    #>

    [OutputType([System.Reflection.Emit.MethodBuilder])]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [System.Reflection.Emit.TypeBuilder]
        $TypeBuilder,

        [Parameter(Mandatory=$true)]
        [System.String]
        $DllName,

        [Parameter(Mandatory=$true)]
        [System.String]
        $Name,

        [Parameter(Mandatory=$true)]
        [Object]
        $ReturnType,

        [Object[]]
        $ParameterTypes = [Object[]]@(),

        [Switch]
        $SetLastError,

        [Runtime.InteropServices.CharSet]
        $CharSet = [System.Runtime.InteropServices.CharSet]::Auto,

        [Switch]$PassThru
    )

    # ParameterTypes is either an array of Type or Hashtable where the Hashtable can contain
    # Ref = Whether the parameter is an in/out parameter
    # Type = The actual type of the parameter, this must be set when using a hashtable
    # MarshalAs = A hashtable that defines the custom MarshalAs attributes. See Set-MarshalAsAttribute
    $parameter_types = [System.Collections.Generic.List`1[Type]]@()
    $parameter_marshal_as = @{}
    for ($i = 0; $i -lt $ParameterTypes.Length; $i++) {
        $parameter = $ParameterTypes[$i]
        if ($parameter -is [System.Collections.IDictionary]) {
            if ($parameter.ContainsKey('Ref') -and $parameter.Ref) {
                $parameter_types.Add(($parameter.Type -as [Type]).MakeByRefType())
            } else {
                $parameter_types.Add(($parameter.Type -as [Type]))
            }

            if ($parameter.ContainsKey('MarshalAs')) {
                $parameter_idx = $i + 1
                $parameter_marshal_as.$parameter_idx = $parameter.MarshalAs
            }
        } else {
            $parameter_types.Add(($parameter -as [Type]))
        }
    }

    # Cast the defined return type as a type. Check if the parameter is a Hashtable that defines the MarshalAs attr
    $marshal_as = $null
    if ($ReturnType -is [System.Collections.IDictionary]) {
        $return_type = $ReturnType.Type -as [Type]

        if ($ReturnType.ContainsKey('MarshalAs')) {
            $marshal_as = $ReturnType.MarshalAs
        }
    } else {
        $return_type = $ReturnType -as [Type]
    }

    # Next, the method is created where we specify the name, parameters and
    # return type that is expected
    $method_builder = $TypeBuilder.DefineMethod(
        $Name,
        [System.Reflection.MethodAttributes]'Public, Static',
        $return_type,
        $parameter_types
    )

    # Set the retval MarshalAs attribute if set
    if ($null -ne $marshal_as) {
        $parameter_builder = $method_builder.DefineParameter(
            0, [System.Reflection.ParameterAttributes]::Retval, $null
        )
        Set-MarshalAsAttribute -Builder $parameter_builder @marshal_as
    }

    # Set the parameter MarshalAs attribute if set
    foreach ($marshal_info in $parameter_marshal_as.GetEnumerator()) {
        $param_idx = $marshal_info.Key
        $marshal_as = $marshal_info.Value
        $parameter_builder = $method_builder.DefineParameter(
            $param_idx,
            [System.Reflection.ParameterAttributes]::None,
            $null
        )
        Set-MarshalAsAttribute -Builder $parameter_builder @marshal_as
    }

    # Set the DllImport() attributes; SetLastError and CharSet
    $dll_ctor = [System.Runtime.InteropServices.DllImportAttribute].GetConstructor([System.String])
    $method_fields = [System.Reflection.FieldInfo[]]@(
        [System.Runtime.InteropServices.DllImportAttribute].GetField('SetLastError'),
        [System.Runtime.InteropServices.DllImportAttribute].GetField('CharSet')
    )
    $method_fields_values = [Object[]]@($SetLastError.IsPresent, $CharSet)
    $dll_import_attr = New-Object -TypeName System.Reflection.Emit.CustomAttributeBuilder -ArgumentList @(
        $dll_ctor,
        $DllName,
        $method_fields,
        $method_fields_values
    )
    $method_builder.SetCustomAttribute($dll_import_attr)

    if ($PassThru) {
        $method_builder
    }
}

Function Import-Struct {
    <#
    .SYNOPSIS
    Defines a struct.
 
    .DESCRIPTION
    Defines a struct in the ModuleBuilder specified.
 
    .PARAMETER ModuleBuilder
    The ModuleBuilder to define the struct in.
 
    .PARAMETER Name
    The name of the struct to define.
 
    .PARAMETER Fields
    A list of hashes that define the fields of the struct. Each value in the fields list should be a hash with the
    following keys;
 
        Name: The name of the field.
        Type: The type of the field.
        MarshalAs: Optional hash that defines the MarshalAs attribute based on the Set-MarshalAsAttribute parameters.
            Type: Sets the [MarshalAs(UnmanagedType.)] value for the parameter.
            SizeConst: Optional SizeConst value for MarshalAs.
            ArraySubType: Optional ArraySubType value for MarshalAs.
 
    .PARAMETER PackingSize
    Set the Pack value for the StructLayout attribute.
 
    .EXAMPLE
    $module_builder = New-DynamicModule -Name PInvokeHelper
 
    $struct_definition = @{
        Name = 'TOKEN_PRIVILEGES'
        Fields = @(
            @{
                Name = 'PrivilegeCount'
                Type = ([System.UInt32])
            },
            @{
                Name = 'Privileges'
                Type = 'LUID_AND_ATTRIBUTES[]'
                MarshalAs = @{
                    Type = [System.Runtime.InteropServices.UnmanagedType]::ByValArray
                    SizeConst = 1
                }
            }
        )
    }
    Import-Struct -ModuleBuilder $module_builder @struct_definition
    #>

    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [System.Reflection.Emit.ModuleBuilder]
        $ModuleBuilder,

        [Parameter(Mandatory=$true)]
        [System.String]
        $Name,

        [Parameter(Mandatory=$true)]
        [Hashtable[]]
        $Fields,

        [System.Reflection.Emit.PackingSize]
        $PackingSize = [System.Reflection.Emit.PackingSize]::Unspecified
    )

    $struct_builder = $ModuleBuilder.DefineType(
        $Name,
        'Class, Public, Sealed, BeforeFieldInit, AutoLayout, SequentialLayout',
        [System.ValueType],
        $PackingSize
    )

    foreach ($field in $Fields) {
        # Make sure we cast the Type key for the field as an actual [Type]. This allows the Type definition to be
        # defined as a string and only casted when it's actually needed.
        $field_type = $field.Type -as [Type]

        # Add a custom MarshalAs attribute if required. This should contain the Type and the optional SizeConst key.
        if ($field.ContainsKey('MarshalAs')) {
            $field_builder = $struct_builder.DefineField($field.Name, $field_type, 'Public, HasFieldMarshal')
            $marshal_as = $field.MarshalAs
            Set-MarshalAsAttribute -Builder $field_builder @marshal_as
        } else {
            $struct_builder.DefineField($field.Name, $field_type, 'Public') > $null
        }
    }

    $struct_builder.CreateType() > $null
}

Function New-DynamicModule {
    <#
    .SYNOPSIS
    Creates a dynamic module.
 
    .DESCRIPTION
    Creates a dynamic module that can then be used by other functions in PInvokeHelper when defining other structs,
    enums, and methods.
 
    .PARAMETER Name
    The unique assembly and module name to define the builder in. The assembly with be "$($Name)Assembly" and the
    module will be "$($Name)Module".
 
    .OUTPUTS
    Creates a dynamic module that can be used to define further types like classes, enums, structs and so forth.
 
    .EXAMPLE
    New-DynamicModule -Name PInvokeHelper
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification="This does make a system change")]
    [OutputType([System.Reflection.Emit.ModuleBuilder])]
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory=$true)]
        [System.String]
        $Name
    )

    $assembly_name = "$($Name)Assembly"
    $module_name = "$($Name)Module"

    $domain = [System.AppDomain]::CurrentDomain

    # Create the dynamic assembly that contains our new module. We set the access to Run so that we don't save the
    # defined types to disk.
    $dynamic_assembly = New-Object -TypeName System.Reflection.AssemblyName -ArgumentList $assembly_name
    $assembly_builder = $domain.DefineDynamicAssembly(
        $dynamic_assembly,
        [System.Reflection.Emit.AssemblyBuilderAccess]::Run
    )

    $dynamic_module = $assembly_builder.DefineDynamicModule($module_name, $false)

    return $dynamic_module
}

$public_functions = @('Get-Win32ErrorMessage', 'Import-Enum', 'Import-PInvokeMethod', 'Import-Struct', 'New-DynamicModule')

<#
Defines the PInvokeHelper.SafeNativeHandle class that guarantees a native handle to be closed. The C# type looks like
 
    public class SafeNativeHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public SafeNativeHandle() : base(true) { }
        public SafeNativeHandle(IntPtr handle) : base(true) { this.handle = handle; }
 
        public static implicit operator IntPtr(SafeNativeHandle h) { return h.DangerousGetHandle(); }
 
        [DllImport("Kernel32.dll", SetLastError = true)]
        private static extern bool CloseHandle(
            IntPtr pObject);
 
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        protected override bool ReleaseHandle()
        {
            return CloseHandle(handle);
        }
    }
#>


$module_builder = New-DynamicModule -Name PInvokeHelper

$type_builder = $module_builder.DefineType(
    'PInvokeHelper.SafeNativeHandle',
    [System.Reflection.TypeAttributes]'AutoLayout, AnsiClass, Class, Public, BeforeFieldInit',
    [Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid]
)

# Get the base constructor and handle field objects for use in the IL method definition
$base_type = [Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid]
$base_ctor = $base_type.GetConstructor(
    [System.Reflection.BindingFlags]'Instance, NonPublic',
    $null,
    [Type[]]@(,[System.Boolean]),
    $null
)
$base_handle_field = $base_type.GetField('handle', [System.Reflection.BindingFlags]'Instance, NonPublic')

# Define the constructor blocks
$ctor1 = $type_builder.DefineConstructor(
    [System.Reflection.MethodAttributes]'PrivateScope, Public, HideBySig, SpecialName, RTSpecialName',
    [System.Reflection.CallingConventions]'Standard, HasThis',
    @()
)
$ctor1_il = $ctor1.GetILGenerator()
$ctor1_il.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0)
$ctor1_il.Emit([System.Reflection.Emit.OpCodes]::Ldc_I4, 1)  # Sets $true onto stack
$ctor1_il.Emit([System.Reflection.Emit.OpCodes]::Call, $base_ctor)  # calls base(true)
$ctor1_il.Emit([System.Reflection.Emit.OpCodes]::Ret)

$ctor2 = $type_builder.DefineConstructor(
    [System.Reflection.MethodAttributes]'PrivateScope, Public, HideBySig, SpecialName, RTSpecialName',
    [System.Reflection.CallingConventions]'Standard, HasThis',
    [Type[]]@(,[System.IntPtr])
)
$ctor2_il = $ctor2.GetILGenerator()
$ctor2_il.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0)
$ctor2_il.Emit([System.Reflection.Emit.OpCodes]::Ldc_I4, 1)
$ctor2_il.Emit([System.Reflection.Emit.OpCodes]::Call, $base_ctor)
$ctor2_il.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0)
$ctor2_il.Emit([System.Reflection.Emit.OpCodes]::Ldarg_1)  # Loads the IntPtr arg passed into the constructor
$ctor2_il.Emit([System.Reflection.Emit.OpCodes]::Stfld, $base_handle_field)  # Sets the handle field with param1
$ctor2_il.Emit([System.Reflection.Emit.OpCodes]::Ret)

# Define the CloseHandle method
$close_handle_builder = $type_builder.DefineMethod(
    'CloseHandle',
    [System.Reflection.MethodAttributes]'Private, Static',
    [System.Boolean],
    [Type[]]@([System.IntPtr])
)

$dll_import_attr = New-Object -TypeName System.Reflection.Emit.CustomAttributeBuilder -ArgumentList @(
    [System.Runtime.InteropServices.DllImportAttribute].GetConstructor([System.String]),
    'Kernel32.dll',
    [System.Reflection.FieldInfo[]]@([System.Runtime.InteropServices.DllImportAttribute].GetField('SetLastError')),
    [Object[]]@($true)
)
$close_handle_builder.SetCustomAttribute($dll_import_attr)

# Define the ReleaseHandle() method
$method_builder = $type_builder.DefineMethod(
    'ReleaseHandle',
    [System.Reflection.MethodAttributes]'PrivateScope, Family, Virtual, HideBySig',
    [System.Boolean],
    @()
)
$method_il = $method_builder.GetILGenerator()
$method_il.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0)
$method_il.Emit([System.Reflection.Emit.OpCodes]::Ldfld, $base_handle_field)  # Load the value of the handle field
$method_il.Emit([System.Reflection.Emit.OpCodes]::Call, $close_handle_builder)  # Call the CloseHandle method
$method_il.Emit([System.Reflection.Emit.OpCodes]::Ret)

# Set [ReliabilityContract(Consistency.WillNotCorrupState, Cer.MayFail)] on the ReleaseHandle() method
$reliability_attr = [System.Runtime.ConstrainedExecution.ReliabilityContractAttribute].GetConstructor(
    [Type[]]@([System.Runtime.ConstrainedExecution.Consistency], [System.Runtime.ConstrainedExecution.Cer])
)
$ca = New-Object -TypeName System.Reflection.Emit.CustomAttributeBuilder -ArgumentList @(
    $reliability_attr,
    @(
        [System.Runtime.ConstrainedExecution.Consistency]::WillNotCorruptState,
        [System.Runtime.ConstrainedExecution.Cer]::MayFail
    )
)
$method_builder.SetCustomAttribute($ca)

# Define implicit operator to IntPtr. This allows the SafeNativeHandle to be used in place of IntPtr without any
# explicit casts. Technically it is dangerous but we are using this type to make sure the handle is disposed.
$dangerous_get_handle = $base_type.GetMethod('DangerousGetHandle')
$impl_method = $type_builder.DefineMethod(
    'op_Implicit',
    [System.Reflection.MethodAttributes]'Public, HideBySig, SpecialName, Static',
    [System.IntPtr],
    @(,$type_builder)
)
$impl_il = $impl_method.GetILGenerator()
$impl_il.Emit([System.Reflection.Emit.OpCodes]::Ldarg_0)
$impl_il.Emit([System.Reflection.Emit.OpCodes]::Callvirt, $dangerous_get_handle)
$impl_il.Emit([System.Reflection.Emit.OpCodes]::Ret)

$type_builder.CreateType() > $null

Export-ModuleMember -Function $public_functions