Skip to content

Instantly share code, notes, and snippets.

@nathan-alden-sr
Last active June 24, 2020 15:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nathan-alden-sr/63dea27baeed8dc47eb9fb4f3def7614 to your computer and use it in GitHub Desktop.
Save nathan-alden-sr/63dea27baeed8dc47eb9fb4f3def7614 to your computer and use it in GitHub Desktop.
Scans a C header file for common macro patterns and converts them to C# compatible with TerraFX bindings.
<#
Copyright 2020 Nathan Alden, Sr.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#>
Set-StrictMode -Version 2
$ErrorActionPreference = "Stop"
function Convert-Macros {
[OutputType([Hashtable])]
param(
[Parameter(Mandatory=$true)]
[object[]] $Lines
)
function replaceCharacterEscapes {
[OutputType([string])]
param(
[string] $String
)
$StringBuilder = New-Object System.Text.StringBuilder $String
# Replace simple character escapes
$StringBuilder.Replace("\a", "`a") > $null
$StringBuilder.Replace("\b", "`b") > $null
$StringBuilder.Replace("\e", "`e") > $null
$StringBuilder.Replace("\f", "`f") > $null
$StringBuilder.Replace("\n", "`n") > $null
$StringBuilder.Replace("\r", "`r") > $null
$StringBuilder.Replace("\t", "`t") > $null
$StringBuilder.Replace("\v", "`v") > $null
$StringBuilder.Replace("\\", "\") > $null
$StringBuilder.Replace("\'", "'") > $null
$StringBuilder.Replace("\`"", "`"") > $null
$StringBuilder.Replace("\?", "?") > $null
# Replace \###, \x##, and \u####
[Regex]::Matches($StringBuilder.ToString(), "((?<Escape>\\)(?<Number>\d{1,3})|(?<Escape>\\x)(?<Number>[0-9A-Fa-f]{2})|(?<Escape>\\u)(?<Number>[0-9A-Fa-f]{4})|(?<Escape>\\U)(?<Number>[0-9A-Fa-f]{8}))") |
Sort-Object -Property Index -Descending |
ForEach-Object {
$Match = $_
switch ($Match.Groups["Escape"].Value) {
"\" {
$StringBuilder.Remove($Match.Index, $Match.Length) > $null
$StringBuilder.Insert($Match.Index, [char][Convert]::ToByte($Match.Groups["Number"].Value, 8)) > $null
}
"\x" {
$StringBuilder.Remove($Match.Index, $Match.Length) > $null
$StringBuilder.Insert($Match.Index, [char][Convert]::ToByte($Match.Groups["Number"].Value, 16)) > $null
}
"\u" {
$StringBuilder.Remove($Match.Index, $Match.Length) > $null
$StringBuilder.Insert($Match.Index, [char][Convert]::ToInt32($Match.Groups["Number"].Value, 16)) > $null
}
"\U" {
# UTF-32 not supported
return $null
}
}
}
$StringBuilder.ToString()
}
function serializeString {
[OutputType([string])]
param(
[string] $Name,
[string] $String
)
# UTF-16 strings should be serialized verbatim
"public const string $Name = `"$String`";"
}
function serializeSByteSpan {
[OutputType([string])]
param(
[string] $Name,
[string] $String
)
$Bytes = New-Object System.Collections.Generic.List[byte] @(,([System.Text.Encoding]::UTF8.GetBytes($String)))
$Bytes.Add(0);
$HexLiterals = $Bytes | ForEach-Object { "0x$($_.ToString("X2").ToUpperInvariant())" }
# UTF-8 strings should be serialized as sbyte arrays and exposed as ReadOnlySpan<sbyte> for optimization purposes
"// $String"
"public static ReadOnlySpan<sbyte> $Name => new sbyte[] { $($HexLiterals -join ", ") };"
}
function parseNumericLiteral {
[OutputType([string])]
param(
[ValidateSet("0b", "0", "0x", "")]
[string] $Prefix,
[ValidateSet("+", "-", "")]
[string] $Sign,
[string] $Number,
[ValidateSet("ull", "ll", "ul", "l", "u", "", IgnoreCase=$true)]
[string] $Suffix,
[string] $TypeOverride = $null
)
<#
C Expression C# Type C# Expression
---------------------------------------------------------------
0x7FFFFFFF int 0x7FFFFFFF
0x80000000 uint 0x80000000
0x7FFFFFFFFFFFFFFF long 0x7FFFFFFFFFFFFFFF
0x8000000000000000 ulong 0x8000000000000000
0x7FFFFFFFL int 0x7FFFFFFF
0x80000000L int unchecked((int)0x80000000)
0x7FFFFFFF00000000LL long 0x7FFFFFFFFFFFFFFF
0x8000000000000000LL long unchecked((long)0x8000000000000000)
0x7FFFFFFFU uint 0x7FFFFFFF
0x80000000U uint 0x80000000
0x7FFFFFFFUL uint 0x7FFFFFFF
0x80000000UL uint 0x80000000
0x7FFFFFFFFFFFFFFFULL ulong 0x7FFFFFFFFFFFFFFF
0xFFFFFFFFFFFFFFFFULL ulong 0xFFFFFFFFFFFFFFFF
#>
switch ($Prefix) {
"0b" {
$ConvertedNumber = [Convert]::ToUInt64($Number, 2)
}
"0" {
$ConvertedNumber = [Convert]::ToUInt64($Number, 8)
# C# does not support octal notation, so change the source number to decimal
$Number = $ConvertedNumber
}
"0x" {
$ConvertedNumber = [Convert]::ToUInt64($Number, 16)
$Number = $Number.ToUpperInvariant()
}
"" {
$ConvertedNumber = [ulong]::Parse($Number)
}
}
$IsUnchecked = $false
switch ($Suffix) {
"" {
if ($ConvertedNumber -gt [long]::MaxValue) {
# 0x8000000000000000 to 0xFFFFFFFFFFFFFFFF
$Type = "ulong"
} elseif ($ConvertedNumber -gt [uint]::MaxValue) {
# 0x100000000‬ to 0x7FFFFFFFFFFFFFFF
$Type = "long"
} elseif ($ConvertedNumber -gt [int]::MaxValue) {
# 0x80000000 to 0xFFFFFFFF
$Type = "uint"
} else {
# 0x0 to 0x7FFFFFFF
$Type = "int"
}
}
"l" {
$Type = "int"
$IsUnchecked = $ConvertedNumber -gt [int]::MaxValue
}
"ll" {
$Type = "long"
$IsUnchecked = $ConvertedNumber -gt [long]::MaxValue
}
{ $Suffix -iin "ul","u" } {
$Type = "uint"
}
"ull" {
$Type = "ulong"
}
}
$Type = $TypeOverride ? $TypeOverride : $Type
$Expression = $IsUnchecked -and !$Type.StartsWith("u") ? "unchecked(($Type)$Prefix$Sign$Number)" : "$Prefix$Sign$Number"
@{
Type = $Type
Expression = $Expression
}
}
$ManualDefines = New-Object System.Collections.Generic.List[Hashtable]
$LineNumber = 0
function addManualDefine {
param(
[string] $Line
)
$ManualDefines.Add(@{
LineNumber = $LineNumber
Line = $Line
})
}
$Declarations = $Lines | ForEach-Object {
$LineNumber++
if ($_ -imatch "^\s*#\s*define\s+(?<Name>\S+)\s+(?<IsWide>L?)\`"(?<String>.*?)\""") {
# Match "" or L"" string literals
$Name = $Matches["Name"]
$IsWide = $Matches["IsWide"].Length -gt 0
$String = $Matches["String"]
if ($IsWide) {
serializeString $Name $String
""
return
} else {
$String = replaceCharacterEscapes $String
if ($String) {
serializeSByteSpan $Name $String
""
return
}
}
addManualDefine $_
} elseif ($_ -imatch "^\s*#\s*define\s+(?<Name>\S+)\s+(?<Prefix>0b|0x|0(?=\d))?(?<Sign>[+-])?(?<Number>[0-9a-f]+)(?<Suffix>u|l|ul|ll|ull)?(\s|//|$)") {
# Match simple numeric literals
# #define CONST -0x12345L
$Name = $Matches["Name"]
$Prefix = $Matches.Contains("Prefix") ? $Matches["Prefix"] : ""
$Sign = $Matches.Contains("Sign") ? $Matches["Sign"] : $null
$Number = $Matches["Number"]
$Suffix = $Matches.Contains("Suffix") ? $Matches["Suffix"] : $null
$Result = parseNumericLiteral $Prefix $Sign $Number $Suffix
"public const $($Result.Type) $Name = $($Result.Expression);"
""
} elseif ($_ -imatch "^\s*#\s*define\s+(?<Name>\S+)\s+\(\s*\(\s*HRESULT\s*\)\s*(?<Prefix>0b|0x|0(?=\d))?(?<Sign>[+-])?(?<Number>[0-9a-f]+)(?<Suffix>u|l|ul|ll|ull)?\s*\)(\s|//|$)") {
# Match (HRESULT()) numeric literals
# #define CONST ((HRESULT)0x1234ABCD)
$Name = $Matches["Name"]
$Prefix = $Matches.Contains("Prefix") ? $Matches["Prefix"] : ""
$Sign = $Matches.Contains("Sign") ? $Matches["Sign"] : $null
$Number = $Matches["Number"]
$Suffix = $Matches.Contains("Suffix") ? $Matches["Suffix"] : $null
$Result = parseNumericLiteral $Prefix $Sign $Number $Suffix
"public const $($Result.Type) $Name = $($Result.Expression);"
""
} elseif ($_ -imatch "^\s*#\s*define\s+(?<Name>\S+)\s+_HRESULT_TYPEDEF_\s*\((?<Prefix>0b|0x|0(?=\d))?(?<Sign>[+-])?(?<Number>[0-9a-f]+)(?<Suffix>u|l|ul|ll|ull)?\s*\)(\s|//|$)") {
# Match _HRESULT_TYPEDEF_ numeric literals
# #define CONST _HRESULT_TYPEDEF_(0x1234ABCDL)
$Name = $Matches["Name"]
$Prefix = $Matches.Contains("Prefix") ? $Matches["Prefix"] : ""
$Sign = $Matches.Contains("Sign") ? $Matches["Sign"] : $null
$Number = $Matches["Number"]
$Suffix = $Matches.Contains("Suffix") ? $Matches["Suffix"] : $null
$Result = parseNumericLiteral $Prefix $Sign $Number $Suffix
"public const $($Result.Type) $Name = $($Result.Expression);"
""
} elseif ($_ -imatch "^\s*#\s*define\s+(?<Name>\S+)\s+_NDIS_ERROR_TYPEDEF_\s*\((?<Prefix>0b|0x|0(?=\d))?(?<Sign>[+-])?(?<Number>[0-9a-f]+)(?<Suffix>u|l|ul|ll|ull)?\s*\)(\s|//|$)") {
# Match _NDIS_ERROR_TYPEDEF_ numeric literals
# #define CONST _NDIS_ERROR_TYPEDEF_(0x1234ABCDL)
$Name = $Matches["Name"]
$Prefix = $Matches.Contains("Prefix") ? $Matches["Prefix"] : ""
$Sign = $Matches.Contains("Sign") ? $Matches["Sign"] : $null
$Number = $Matches["Number"]
$Suffix = $Matches.Contains("Suffix") ? $Matches["Suffix"] : $null
$Result = parseNumericLiteral $Prefix $Sign $Number $Suffix "uint"
"public const $($Result.Type) $Name = $($Result.Expression);"
""
} elseif ($_ -imatch "^\s*#\s*define\s") {
# Manual defines
addManualDefine $_
}
}
@{
Declarations = $Declarations
ManualDefines = $ManualDefines
}
}
# Convert a header file
$Contents = Get-Content "C:\Program Files (x86)\Windows Kits\10\Include\10.0.18362.0\um\WinUser.h"
# Convert arbitrary content
# $Contents = @()
$Result = Convert-Defines $Contents
$StringBuilder = New-Object System.Text.StringBuilder
function appendLine {
param(
[string] $Line = $null,
[int] $Spaces = 8
)
if ($Line) {
$StringBuilder.Append("$(" " * $Spaces)$Line`n") > $null
} else {
$StringBuilder.Append("`n") > $null
}
}
foreach ($Declaration in $Result.Declarations) {
appendLine $Declaration
}
if ($Result.ManualDefines) {
$MaximumLineNumberCharacters = ($Result.ManualDefines | ForEach-Object { $_.LineNumber.ToString().Length } | Measure-Object -Maximum).Maximum
foreach ($ManualDefine in $Result.ManualDefines) {
appendLine "// Line $($ManualDefine.LineNumber): $(" " * ($MaximumLineNumberCharacters - $ManualDefine.LineNumber.ToString().Length))$($ManualDefine.Line)"
}
}
$StringBuilder.ToString() | Set-Clipboard
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment