lib/delegate.ps1

#requires -version 6
using namespace System.Reflection
using namespace System.Reflection.Emit
using namespace System.Linq.Expressions
using namespace System.Runtime.InteropServices

function Get-ProcAddress {
  [OutputType([Hashtable])]
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateNotNullOrEmpty()]
    [String]$Module,

    [Parameter(Mandatory, Position=1)]
    [ValidateNotNullOrEmpty()]
    [String[]]$Function
  )

  process {
    $kernel32 = @{}

    [Assembly]::LoadFile("$(
      [RuntimeEnvironment]::GetRuntimeDirectory()
    )Microsoft.Win32.SystemEvents.dll"
).GetType('Interop').GetNestedType(
      'Kernel32', [BindingFlags]'NonPublic'
    ).GetMethods([BindingFlags]'NonPublic, Static, Public').Where{
      $_.Name -cmatch '\AGet(Proc|Mod)'
    }.ForEach{ $kernel32[$_.Name] = $_ }

    if ((
      $mod = $kernel32.GetModuleHandle.Invoke($null, @($Module))
    ) -eq [IntPtr]::Zero) { throw [DllNotFoundException]::new() }

    $funcs = @{}
    $Function.ForEach{
      if ((
        $$ = $kernel32.GetProcAddress.Invoke($null, @($mod, $_))
      ) -ne [IntPtr]::Zero) { $funcs.$_ = $$ }
    }
    $funcs
  }
}

function Set-Delegate {
  [OutputType([Type])]
  [CmdletBinding(DefaultParameterSetName='Prototype')]
  param(
    [Parameter(Mandatory, ParameterSetName='Prototype', Position=0)]
    [ValidateNotNull()]
    [Alias('p')]
    [Type]$Prototype,

    [Parameter(Mandatory, ParameterSetName='PrototypeAsTypeArray', Position=0)]
    [ValidateNotNullOrEmpty()]
    [Alias('pa')]
    [Type[]]$PrototypeAsTypeArray,

    [Parameter(Mandatory, Position=1)]
    [ValidateScript({$_ -ne [IntPtr]::Zero})]
    [IntPtr]$Address,

    [Parameter(Position=2)]
    [ValidateNotNullOrEmpty()]
    [CallingConvention]$CallingConvention = 'StdCall'
  )

  process {
    switch ($PSCmdlet.ParameterSetName) {
      'Prototype'            {
        $method = $Prototype.GetMethod('Invoke')
        $returntype, $paramtypes = $method.ReturnType, $method.GetParameters().ParameterType
        $paramtypes = ($paramtypes, $null)[!$paramtypes]
        $il, $sz = ($holder = [DynamicMethod]::new(
          'Invoke', $returntype, $paramtypes, $Prototype
        )).GetILGenerator(), [IntPtr]::Size

        if ($paramtypes) {
          (0..($paramtypes.Length - 1)).ForEach{ $il.Emit([OpCodes]::ldarg, $_) }
        }

        $il.Emit([OpCodes]::"ldc_i$sz", $Address."ToInt$((32, 64)[$sz / 4 - 1])"())
        $il.EmitCalli([OpCodes]::calli, $CallingConvention, $returntype, $paramtypes)
        $il.Emit([OpCodes]::ret)

        $holder.CreateDelegate($Prototype)
      }
      'PrototypeAsTypeArray' {
        [Marshal]::GetDelegateForFunctionPointer(
          $Address, [Delegate]::CreateDelegate(
            [Func[[Type[]], Type]],
            [Expression].Assembly.GetType(
              'System.Linq.Expressions.Compiler.DelegateHelpers'
            ).GetMethod(
              'MakeNewCustomDelegate', [BindingFlags]'NonPublic, Static'
            )
          ).Invoke($PrototypeAsTypeArray)
        )
      }
    } # switch
  }
}

function New-Delegate {
  [OutputType([Hashtable])]
  [CmdletBinding()]
  param(
    [Parameter(Mandatory, Position=0)]
    [ValidateNotNullOrEmpty()]
    [String]$Module,

    [Parameter(Mandatory, Position=1)]
    [ValidateNotNull()]
    [Hashtable]$Signature
  )

  process {
    $funcs, $addr = @{}, (Get-ProcAddress -Module $Module -Function $Signature.Keys)
    $addr.Keys.ForEach{
      $ptr, $sig = $addr.$_, $Signature.$_
      if (!$sig) { throw [InvalidOperationException]::new() }
      $funcs.$_ = switch -Regex ($sig.Name) {
        '\A(Action|Func)' { Set-Delegate -Address $ptr -Prototype $sig }
        default { Set-Delegate -Address $ptr -PrototypeAsTypeArray $sig }
      }
    }
    $funcs
  }
}