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.github.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