Last active
September 14, 2025 08:42
-
-
Save anonhostpi/e33c2fb4e3282ff75962cf12a2a9af6a to your computer and use it in GitHub Desktop.
How to do wasm in powershell
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# iex (iwr 'https://gist.githubusercontent.com/anonhostpi/e33c2fb4e3282ff75962cf12a2a9af6a/raw/wasm.ps1').content | |
& { | |
# Install-Package "Wasmtime" -ProviderName NuGet | |
$package = Get-Package -Name "Wasmtime" | |
$directory = $package.Source | Split-Path | |
$runtime = "win-x64" # "win/linux/osx-arm64/x64" | |
$native = "$directory\runtimes\$runtime\native" | Resolve-Path | |
$env:PATH += ";$native" | |
Add-Type -Path "$directory\lib\netstandard2.1\Wasmtime.Dotnet.dll" | |
} | |
function New-WasmEngine { | |
[CmdletBinding()] | |
param( | |
[Parameter(ValueFromPipeline=$true)] | |
[Wasmtime.Config] $config = $null | |
) | |
If ($null -eq $config) { | |
return [Wasmtime.Engine]::new() | |
} else { | |
return [Wasmtime.Engine]::new($config) | |
} | |
} | |
function ConvertTo-Wasm { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
[string] $Text | |
) | |
return [Wasmtime.Module]::ConvertText($Text) | |
} | |
function New-WasmModule { | |
[CmdletBinding(DefaultParameterSetName='InputObject')] | |
param ( | |
[Parameter(Mandatory=$true)] | |
[Wasmtime.Engine] $Engine, | |
[Parameter(ParameterSetName='URL', Mandatory=$true)] | |
[string] $Url, | |
[Parameter(ParameterSetName='URL')] | |
[Parameter(ParameterSetName='InputObject', Mandatory=$true)] | |
[string] $Name, | |
[Parameter(ParameterSetName='InputObject', Mandatory=$true, ValueFromPipeline=$true)] | |
$InputObject, | |
[Parameter(ParameterSetName='URL')] | |
[Parameter(ParameterSetName='InputObject')] | |
[switch] $Binary, # Default is .wat (text) | |
[Parameter(ParameterSetName='InputObject')] | |
[switch] $Stream, | |
[Parameter(ParameterSetName='File', Mandatory=$true, ValueFromPipeline=$true)] | |
[string] $Path, | |
[Parameter(ParameterSetName='URL')] | |
[Parameter(ParameterSetName='File')] | |
[switch] $Text # Default is .wasm (binary) | |
) | |
$uri = $Url | |
$URLProvided = & { | |
If( $PSCmdlet.ParameterSetName -eq 'URL' ) { | |
return $true | |
} | |
If( $PSCmdlet.ParameterSetName -eq 'InputObject' ) { | |
If( [string]::IsNullOrWhiteSpace($InputObject) ){ | |
return $false | |
} | |
Try { | |
$uri = [System.Uri]::new($InputObject) | |
return $uri.IsAbsoluteUri -and ($uri.Scheme -in @('http', 'https')) | |
} Catch { | |
return $false | |
} | |
} | |
If( $PSCmdlet.ParameterSetName -eq 'File' ) { | |
If( [string]::IsNullOrWhiteSpace($Path) ){ | |
return $false | |
} | |
Try { | |
return -not (Test-Path $Path -PathType Leaf) | |
} Catch {} | |
Try { | |
$uri = [System.Uri]::new($Path) | |
return $uri.IsAbsoluteUri -and ($uri.Scheme -eq 'file') | |
} Catch { | |
return $false | |
} | |
} | |
} | |
If( $URLProvided ){ | |
If([string]::IsNullOrEmpty($Name)){ | |
$Name = [System.IO.Path]::GetFileNameWithoutExtension("$uri") | |
} | |
$request = [System.Net.WebRequest]::Create("$uri") | |
$response = $request.GetResponse() | |
$IsBinary = & { | |
$switches = @([bool]$Binary, [bool]$Text) | Where-Object { $_ -eq $true } | |
If($switches.Count -eq 1){ | |
return $Binary | |
} | |
$extension = [System.IO.Path]::GetExtension("$uri").ToLowerInvariant() | |
switch ($extension) { | |
'.wasm' { return $true } | |
'.wat' { return $false } | |
default { | |
switch($response.ContentType.ToLowerInvariant()) { | |
'text/plain' { return $false } | |
'text/wat' { return $false } | |
'application/wat' { return $false } | |
default { return $true } # assume anything else is binary | |
} | |
} | |
} | |
} | |
[System.IO.Stream] $stream = $response.GetResponseStream() | |
If($IsBinary) { | |
return [Wasmtime.Module]::FromStream($Engine, $Name, $stream) | |
} Else { | |
return [Wasmtime.Module]::FromTextStream($Engine, $Name, $stream) | |
} | |
} | |
switch ($PSCmdlet.ParameterSetName) { | |
'InputObject' { | |
If($Binary) { | |
If($Stream) { | |
return [Wasmtime.Module]::FromStream($Engine, $Name, ($InputObject | Select-Object -First 1)) | |
} | |
return [Wasmtime.Module]::FromBytes($Engine, $Name, $InputObject) | |
} Else { | |
If($Stream) { | |
return [Wasmtime.Module]::FromTextStream($Engine, $Name, ($InputObject | Select-Object -First 1)) | |
} | |
return [Wasmtime.Module]::FromText($Engine, $Name, "$InputObject") | |
} | |
} | |
'File' { | |
If($Text) { | |
return [Wasmtime.Module]::FromFileText($Engine, "$Path") | |
} Else { | |
return [Wasmtime.Module]::FromFile($Engine, "$Path") | |
} | |
} | |
} | |
} | |
function New-WasmLinker { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
[Wasmtime.Engine] $Engine, | |
[switch] $Wasi | |
) | |
$linker = [Wasmtime.Linker]::new($Engine) | |
If($Wasi) { | |
$linker.DefineWasi() | Out-Null | |
} | |
return $linker | |
} | |
function New-WasiConfig { | |
[CmdletBinding()] | |
param( | |
$ArgumentList, | |
[switch] $InheritArguments, | |
[System.Collections.IDictionary] $EnvironmentVariables, | |
[switch] $InheritEnvironment, | |
[System.Collections.IDictionary] $DirectoryMounts, | |
[string] $ErrorFile, | |
[ValidateScript({ | |
if ($PSBoundParameters.ContainsKey('ErrorFile')) { | |
throw "You cannot use -ErrorFile and -InheritStandardError together." | |
} | |
$true | |
})] | |
[switch] $InheritStandardError, | |
[string] $OutputFile, | |
[ValidateScript({ | |
if ($PSBoundParameters.ContainsKey('OutputFile')) { | |
throw "You cannot use -OutputFile and -InheritStandardOutput together." | |
} | |
$true | |
})] | |
[switch] $InheritStandardOutput, | |
[string] $InputFile, | |
[ValidateScript({ | |
if ($PSBoundParameters.ContainsKey('InputFile')) { | |
throw "You cannot use -InputFile and -InheritStandardInput together." | |
} | |
$true | |
})] | |
[switch] $InheritStandardInput | |
) | |
$config = [Wasmtime.WasiConfiguration]::new() | |
if ($InheritArguments) { | |
$config.WithInheritedArgs() | Out-Null | |
} | |
$a = $ArgumentList | ForEach-Object { "$_" } | |
If( $a.Count -eq 1 ){ | |
$config.WithArg(($a | Select-Object -First 1)) | Out-Null | |
} | |
If( $a.Count -gt 1 ){ | |
$a = $a | ForEach-Object { $_ | ConvertTo-Json -Compress } | |
$a = $a -join "," | |
Invoke-Expression "`$config.WithArgs($a) | Out-Null" | |
} | |
if ($InheritEnvironment) { | |
$config.WithInheritedEnvironment() | Out-Null | |
} | |
If( $EnvironmentVariables.Count ){ | |
$tuples = $EnvironmentVariables.GetEnumerator() | ForEach-Object { | |
[System.ValueTuple[string,string]]::new($_.Key, $_.Value) | |
} | |
$config.WithEnvironmentVariables($tuples) | Out-Null | |
} | |
if ($InheritStandardError) { | |
$config.WithInheritedStandardError() | Out-Null | |
} elseif( Test-Path -PathType Leaf $ErrorFile ) { | |
$config.WithStandardError("$ErrorFile") | Out-Null | |
} | |
if ($InheritStandardOutput) { | |
$config.WithInheritedStandardOutput() | Out-Null | |
} elseif( Test-Path -PathType Leaf $OutputFile ) { | |
$config.WithStandardOutput("$OutputFile") | Out-Null | |
} | |
if ($InheritStandardInput) { | |
$config.WithInheritedStandardInput() | Out-Null | |
} elseif( Test-Path -PathType Leaf $InputFile ) { | |
$config.WithStandardInput("$InputFile") | Out-Null | |
} | |
If( $DirectoryMounts.Count ){ | |
$DirectoryMounts.GetEnumerator() | ForEach-Object { | |
$dirs = @{ | |
Host = $_.Key | |
Guest = $_.Value | |
} | |
$perms = & { | |
If( $dirs.Guest -is [string] ){ | |
return @{ | |
dir = [Wasmtime.WasiDirectoryPermissions]::Read | |
file = [Wasmtime.WasiFilePermissions]::Read | |
} | |
} | |
$perm_dir, $perm_file = (& { | |
$user_provided = $dirs.Guest.Permissions | |
$has_perms = $null -ne $user_provided | |
If( -not $has_perms ){ return @("Read", "Read") } | |
$has_dir = $null -ne $user_provided.Directory | |
$has_file = $null -ne $user_provided.File | |
If( $has_dir -or $has_file ){ | |
$count = [int]$has_dir + [int]$has_file | |
If( $count -eq 2 ){ | |
return @($user_provided.Directory, $user_provided.File) | |
} | |
If( $has_dir ){ | |
return @($user_provided.Directory, "Read") | |
} | |
If( $has_file ){ | |
return @("Read", $user_provided.File) | |
} | |
} | |
return @($user_provided, $user_provided) | |
}) | |
$full = [System.IO.Path]::GetFullPath($dirs.Guest.Directory) | |
$no_drive = $full -replace '^[a-zA-Z]:', '' | |
$unix = $no_drive.Replace("\", "/") | |
$dirs.Guest = $unix | |
return @{ | |
dir = (& { | |
switch("$perm_dir"){ | |
"Read" { [Wasmtime.WasiDirectoryPermissions]::Read } | |
"R" { [Wasmtime.WasiDirectoryPermissions]::Read } | |
"Write" { [Wasmtime.WasiDirectoryPermissions]::Write } | |
"W" { [Wasmtime.WasiDirectoryPermissions]::Write } | |
"ReadWrite" { [Wasmtime.WasiDirectoryPermissions]::Write } | |
"RW" { [Wasmtime.WasiDirectoryPermissions]::Write } | |
"$([int]([Wasmtime.WasiDirectoryPermissions]::Read))" { [Wasmtime.WasiDirectoryPermissions]::Read } | |
"$([int]([Wasmtime.WasiDirectoryPermissions]::Write))" { [Wasmtime.WasiDirectoryPermissions]::Write } | |
default { | |
[Wasmtime.WasiDirectoryPermissions]::Read | |
} | |
} | |
}) | |
file = (& { | |
switch("$perm_file"){ | |
"Read" { [Wasmtime.WasiFilePermissions]::Read } | |
"R" { [Wasmtime.WasiFilePermissions]::Read } | |
"Write" { [Wasmtime.WasiFilePermissions]::Write } | |
"W" { [Wasmtime.WasiFilePermissions]::Write } | |
"ReadWrite" { [Wasmtime.WasiFilePermissions]::Write } | |
"RW" { [Wasmtime.WasiFilePermissions]::Write } | |
"$([int]([Wasmtime.WasiFilePermissions]::Read))" { [Wasmtime.WasiFilePermissions]::Read } | |
"$([int]([Wasmtime.WasiFilePermissions]::Write))" { [Wasmtime.WasiFilePermissions]::Write } | |
default { | |
[Wasmtime.WasiFilePermissions]::Read | |
} | |
} | |
}) | |
} | |
} | |
$config.WithPreopenedDirectory("$($dirs.Host)", "$($dirs.Guest)", $perms.dir, $perms.file) | Out-Null | |
} | |
} | |
return $config | |
} | |
function New-WasmStore { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
[Wasmtime.Engine] $Engine, | |
[System.Object] $Context = $Null, | |
[Wasmtime.WasiConfiguration] $WasiConfiguration = $Null | |
) | |
$store = If($null -eq $Context){ | |
[Wasmtime.Store]::new($Engine) | |
} else { | |
[Wasmtime.Store]::new($Engine, $Context) | |
} | |
If($null -ne $WasiConfiguration) { | |
$store.SetWasiConfiguration($WasiConfiguration) | |
} | |
return $store | |
} | |
# NOTE: does not support return values. To return values, declare the function explicitly! | |
function New-WasmFunction { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$true)] | |
[Wasmtime.Store] $Store, | |
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
[scriptblock] $Callback, | |
[Type[]] $Parameters = (&{ | |
$callback.Ast.ParamBlock.Parameters.StaticType | |
}) | |
) | |
$cb = If($Parameters.Count -gt 0) { | |
"[System.Action[$(($Parameters | ForEach-Object { $_.FullName }) -join ',')]] `$Callback" | |
} Else { | |
"[System.Action] `$Callback" | |
} | |
return [Wasmtime.Function]::FromCallback($Store, (Invoke-Expression $cb)) | |
} | |
function Get-WasiProxyModule { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory=$true, ValueFromPipeline=$true)] | |
[Wasmtime.Engine] $Engine | |
) | |
New-WasmModule -Engine $Engine -Url 'https://github.com/bytecodealliance/wasmtime/releases/download/v36.0.2/wasi_snapshot_preview1.proxy.wasm' | |
} | |
function Get-WasmLibraryName { | |
[CmdletBinding()] | |
param() | |
return ([Wasmtime.Engine].DeclaredFields | Where-Object { $_.Name -eq "LibraryName" }).GetValue($null) | |
} | |
New-Module "Wabt" -ScriptBlock { | |
# Cache: | |
$wabt = [ordered]@{} | |
function Get-WabtModules { | |
If( $wabt.Keys.Count -eq 0 ){ | |
& { | |
# For temporary tar support | |
# - We can later swap this out for a wasm implementation | |
# Install-Package "SharpZipLib" -RequiredVersion 1.4.2 -ProviderName NuGet | |
$package = Get-Package -Name "SharpZipLib" | |
$directory = $package.Source | Split-Path | |
Add-Type -Path "$directory\lib\netstandard2.1\ICSharpCode.SharpZipLib.dll" | |
} | |
$build = "https://github.com/WebAssembly/wabt/releases/download/1.0.37/wabt-1.0.37-wasi.tar.gz" | |
$request = [System.Net.WebRequest]::Create($build) | |
$response = $request.GetResponse() | |
$stream = $response.GetResponseStream() | |
$gzip = [ICSharpCode.SharpZipLib.GZip.GZipInputStream]::new($stream) | |
$tar = [ICSharpCode.SharpZipLib.Tar.TarInputStream]::new($gzip) | |
while ($true) { | |
$entry = $tar.GetNextEntry() | |
if ($null -eq $entry) { | |
break | |
} | |
if ($entry.IsDirectory) { continue } | |
$path = $entry.Name | |
if (-not ($path.TrimStart("\/").Replace("\", "/") -like "wabt-1.0.37/bin/*")) { continue } | |
$name = [System.IO.Path]::GetFileNameWithoutExtension($path) | |
$data = New-Object byte[] $entry.Size | |
if ($tar.Read($data, 0, $data.Length) -ne $data.Length) { | |
throw "Failed to read full entry: $($entry.Name)" | |
} | |
$wabt[$name] = $data | |
} | |
} | |
return $wabt | |
} | |
$stdout_file = @{ | |
Enabled = $false | |
Path = New-TemporaryFile | |
} | |
function New-WasiRuntime { | |
$runtime = @{ Engine = New-WasmEngine } | |
$wasi_params = @{ | |
ArgumentList = $args | |
InheritEnvironment = $true | |
InheritStandardError = $true | |
InheritStandardInput = $true | |
DirectoryMounts = @{ | |
"$(Get-Location)" = @{ | |
Directory = "/" | |
Permissions = @{ | |
Directory = "Read" | |
File = "Read" | |
} | |
} | |
} | |
} | |
If( $stdout_file.Enabled ){ | |
$wasi_params.OutputFile = $stdout_file.Path | |
} Else { | |
$wasi_params.InheritStandardOutput = $true | |
} | |
$runtime.Store = New-WasmStore ` | |
-Engine $runtime.Engine ` | |
-WasiConfiguration (New-WasiConfig @wasi_params) | |
$runtime.Linker = New-WasmLinker -Engine $runtime.Engine -Wasi | |
return $runtime | |
} | |
function ConvertTo-PascalCase { | |
param( | |
[Parameter(Mandatory)] | |
[string]$InputString | |
) | |
# Step 1: split on non-alphanumeric chars | |
$segments = $InputString -split '[^a-zA-Z0-9]+' | Where-Object { $_ } | |
$parts = foreach ($seg in $segments) { | |
# Step 2: split segment into alternating letter/digit groups | |
[regex]::Split($seg, "(?<=\d)(?=[a-zA-Z])") | Where-Object { $_ } | |
} | |
# Step 3: capitalize each part if it starts with a letter | |
$pascal = ($parts | ForEach-Object { | |
if ($_ -match '^[a-zA-Z]') { | |
$_.Substring(0,1).ToUpper() + $_.Substring(1).ToLower() | |
} else { | |
$_ | |
} | |
}) -join '' | |
return $pascal | |
} | |
$mapping = @{} | |
foreach($name in (Get-WabtModules).Keys) { | |
$functionname = ConvertTo-PascalCase $name | |
$functionname = $functionname.Replace("2","To") | |
$functionname = "Invoke-$functionname" | |
$mapping[$functionname] = $name | |
Set-Item -Path "function:$functionname" -Value { | |
$binary_name = $mapping[$MyInvocation.MyCommand.Name] | |
Clear-Content -Path $stdout_file.Path -ErrorAction SilentlyContinue | |
$stdout_file.Enabled = $true | |
$runtime = New-WasiRuntime $binary_name @args | |
Try { | |
$runtime.Linker.Instantiate( | |
$runtime.Store, | |
[Wasmtime.Module]::FromBytes( | |
$runtime.Engine, | |
$binary_name, | |
$wabt."$binary_name" | |
) | |
).GetFunction("_start").Invoke() | Out-Null | |
} Catch { | |
Write-Warning "Some WASM runtime error occurred. Check the output for details or `$Error." | |
} | |
return Get-Content -Path $stdout_file.Path -ErrorAction SilentlyContinue | |
} | |
Set-Item -Path "function:$functionname`Live" -Value { | |
# We may be able to fix this at a later point by defining overwriting the builtin fd_write behavior | |
# This may be possible with AllowShadowing set to true | |
Write-Warning "Live output can not be captured to a variable or piped!" | |
Write-Host "- Wasmtime internally pipes directly to stdout instead of piping back to C#/PowerShell." | |
Write-Host "- To capture output, use $($MyInvocation.MyCommand.Name.Replace('Live','')) instead." | |
Write-Host | |
$binary_name = $mapping[$MyInvocation.MyCommand.Name.Replace("Live","")] | |
$stdout_file.Enabled = $false | |
$runtime = New-WasiRuntime $binary_name @args | |
Try { | |
$runtime.Linker.Instantiate( | |
$runtime.Store, | |
[Wasmtime.Module]::FromBytes( | |
$runtime.Engine, | |
$binary_name, | |
$wabt."$binary_name" | |
) | |
).GetFunction("_start").Invoke() | Out-Null | |
} Catch { | |
Write-Warning "Some WASM runtime error occurred. Check the output for details or `$Error." | |
} | |
} | |
} | |
Export-ModuleMember -Function ($mapping.Keys | % { $_ } | % { "$_","${_}Live" }) | |
} | Import-Module | |
function Test-Wasm { | |
[CmdletBinding()] | |
param( | |
[Parameter(ValueFromPipeline=$true)] | |
[System.Collections.IDictionary] $Imports = @{ | |
"say" = @{ | |
"hello" = { | |
Write-Host "Hello from wasm!" | |
} | |
} | |
} | |
) | |
$state = @{ Engine = New-WasmEngine } | |
$state.Linker = New-WasmLinker -Engine $state.Engine | |
$state.Store = New-WasmStore ` | |
-Engine $state.Engine ` | |
-WasiConfiguration (New-WasiConfig ` | |
-InheritArguments ` | |
-InheritEnvironment ` | |
-InheritStandardError ` | |
-InheritStandardOutput ` | |
-InheritStandardInput ` | |
-DirectoryMounts @{ | |
"$(Get-Location)" = @{ | |
Directory = "/" | |
Permissions = @{ | |
Directory = "Read" | |
File = "Read" | |
} | |
} | |
} | |
) | |
# -Context @{} | |
$signatures = @() | |
$Imports.GetEnumerator() | ForEach-Object { | |
$from = $_.Key | |
$_.Value.GetEnumerator() | ForEach-Object { | |
$name = $_.Key | |
$function = New-WasmFunction -Store $state.Store -Callback $_.Value | |
$state.Linker.Define($from, $name, $function) | |
$signatures += "$from.$name" | |
} | |
} | |
$state.Module = & { | |
$labels = @() | |
$functions = $signatures | ForEach-Object { | |
$from, $name = $_ -split '\.' | |
$label = '$' + (@($from, $name) -join '_') | |
$labels += $label | |
return "(func $label (import `"$from`" `"$name`"))" | |
} | |
$calls = & { | |
$result = @() | |
$labels | ForEach-Object { | |
$result += "(call $_)" | |
} | |
return $result -join " " | |
} | |
$run = "(func (export `"run`") $calls)" | |
$functions = @($functions, $run) | |
return "(module $($functions -join " "))" | |
} | |
$state.Module = $state.Module | New-WasmModule -Engine $state.Engine -Name "Main" | |
$state.ModuleInstance = $state.Linker.Instantiate($state.Store, $state.Module) | |
$state.ModuleInstance.GetFunction("run").Invoke() | |
$state.LibraryName = Get-WasmLibraryName | |
$state.WASI = Get-WasiProxyModule -Engine $state.Engine | |
return $state | |
} | |
# $test = Test-Wasm |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment