src/DllExportCompiler.ps1
|
# 原生 DLL 导出(#_DllExport / Build.DllExports)实现。 # # 机制:先用 CodeDom 把程序帧编译成普通 .NET 类库,再用 AsmResolver 给每个导出包装方法设置 # UnmanagedExportInfo,由 AsmResolver 的托管 PE 写出器生成 native 导出桩 + CLR vtable fixup # (VTableFromUnmanaged),使产物可被 native 的 LoadLibrary/GetProcAddress 直接加载。 # 这正是 ilasm 对 `.export` 指令所做的事,但省掉了 ildasm→文本→ilasm 的往返与外部进程。 # # AsmResolver 与 ExeSinker 共用 src/bin/AsmResolver 下 illink 裁剪过的程序集;本路径额外用到 # DotNet 层的 module 写出器,相关 API 必须镜像在 tools/AsmResolver/Root.cs 中(见该文件)。 # 惰性加载 AsmResolver(含 DotNet 层)。返回是否可用。 function Import-DllExportAssemblies { if ('AsmResolver.DotNet.ModuleDefinition' -as [type]) { return $true } Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot 'bin/AsmResolver') -Recurse -Filter *.dll | ForEach-Object { try { Add-Type -LiteralPath $_.FullName -ErrorVariable $null } catch { $_.Exception.LoaderExceptions | Out-String | Write-Verbose $Error.Remove($_) } } return [bool]('AsmResolver.DotNet.ModuleDefinition' -as [type]) } # 取导出声明里的字段(哈希表与 PSCustomObject 都支持;两者查找都不区分大小写)。 function Get-DllExportField { param($Entry, [string]$Name, $Default) if ($Entry -is [System.Collections.IDictionary]) { if ($Entry.Contains($Name)) { return $Entry[$Name] } } elseif ($Entry.PSObject.Properties.Match($Name).Count) { return $Entry.$Name } return $Default } # 由导出声明生成 C# 包装方法源码;返回 @{ Code; Map },Map 为 @(@{ Method; Export }) 供 AsmResolver 注入。 function New-DllExportMethods { param([object[]]$Exports) $sb = [System.Text.StringBuilder]::new() $map = @() for ($i = 0; $i -lt $Exports.Count; $i++) { $entry = $Exports[$i] $returnType = "$(Get-DllExportField $entry 'returntype' 'void')".Trim() $funcName = "$(Get-DllExportField $entry 'funcname' '')".Trim() if (-not $funcName) { throw "第 $($i + 1) 个导出缺少函数名" } if ($returnType -match '[\r\n";{}]' -or $funcName -match '[\r\n''"]') { throw "非法的导出声明:$returnType $funcName" } $params = @(Get-DllExportField $entry 'params' @()) $csParams = @() $csArgs = @() for ($p = 0; $p -lt $params.Count; $p++) { $paramEntry = $params[$p] $pType = "$(Get-DllExportField $paramEntry 'type' 'string')".Trim() $pName = "$(Get-DllExportField $paramEntry 'name' "arg$p")".Trim() if ($pName -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') { $pName = "arg$p" } if ($pType -match '[\r\n";{}]') { throw "非法的参数类型:$pType" } $csParams += "$pType $pName" $csArgs += $pName } $methodName = "PS12ExeDllExport$i" $map += @{ Method = $methodName; Export = $funcName } $escapedFunc = $funcName.Replace('\', '\\').Replace('"', '\"') $paramList = ($csParams -join ', ') $argsList = if ($csArgs.Count) { 'new object[] { ' + ($csArgs -join ', ') + ' }' } else { 'new object[0]' } [void]$sb.AppendLine() [void]$sb.AppendLine("`t`tpublic static $returnType $methodName($paramList) {") if ($returnType -ieq 'void') { [void]$sb.AppendLine("`t`t`ttry { DllInitChecker(); InvokePSFunction(`"$escapedFunc`", $argsList); }") [void]$sb.AppendLine("`t`t`tcatch (System.Exception ex) { ReportDllExportError(`"$escapedFunc`", ex); }") } else { [void]$sb.AppendLine("`t`t`ttry { DllInitChecker(); return ($returnType)InvokePSFunction(`"$escapedFunc`", $argsList); }") [void]$sb.AppendLine("`t`t`tcatch (System.Exception ex) { ReportDllExportError(`"$escapedFunc`", ex); return default($returnType); }") } [void]$sb.AppendLine("`t`t}") } return @{ Code = $sb.ToString(); Map = $map } } # 在所有类型(含嵌套)里按名字找方法定义。 function Get-DllExportMethodDefinition { param($Module, [string]$MethodName) foreach ($type in $Module.GetAllTypes()) { foreach ($method in $type.Methods) { if ($method.Name.ToString() -eq $MethodName) { return $method } } } return $null } # 给已编译的托管类库注入原生导出。$Exports 为 New-DllExportMethods 的 Map。 function Add-DllExportsToAssembly { param([string]$AssemblyPath, [object[]]$Exports, [string]$Architecture) if (-not (Import-DllExportAssemblies)) { Write-I18n Error DllExportToolchainFailed 'src/bin/AsmResolver' -Category NotInstalled throw "AsmResolver is unavailable: $PSScriptRoot\bin\AsmResolver" } $module = [AsmResolver.DotNet.ModuleDefinition]::FromFile($AssemblyPath) # x86 用 32 位 vtable 项,x64 用 64 位;二者都配 COR_VTABLE_FROM_UNMANAGED 让 CLR 生成 native 入口桩。 $vtableEnum = [AsmResolver.PE.DotNet.VTableFixups.VTableType] $bitValue = if ($Architecture -eq 'x86') { [int]$vtableEnum::VTable32Bit } else { [int]$vtableEnum::VTable64Bit } $vtableType = [AsmResolver.PE.DotNet.VTableFixups.VTableType]($bitValue -bor [int]$vtableEnum::VTableFromUnmanaged) foreach ($export in $Exports) { $method = Get-DllExportMethodDefinition -Module $module -MethodName $export.Method if (-not $method) { Write-I18n Error DllExportMethodNotFound $export.Method -Category InvalidData throw "export method not found in assembly: $($export.Method)" } $method.ExportInfo = [AsmResolver.DotNet.UnmanagedExportInfo]::new("$($export.Export)", $vtableType) } # 原生导出依赖 mscoree 的 CLR 引导桩:必须去掉 ILOnly,否则 AsmResolver 不会写出 _CorDllMain 导入。 $ilOnly = [int][AsmResolver.PE.DotNet.DotNetDirectoryFlags]::ILOnly $module.Attributes = [AsmResolver.PE.DotNet.DotNetDirectoryFlags](([int]$module.Attributes) -band (-bnot $ilOnly)) $tmp = $AssemblyPath + '.asmresolver.tmp' try { $module.Write($tmp) Remove-Item -LiteralPath $AssemblyPath -Force Move-Item -LiteralPath $tmp -Destination $AssemblyPath -Force } finally { Remove-Item -LiteralPath $tmp -Force -ErrorAction Ignore } } |