Skip to content

Instantly share code, notes, and snippets.

@anonhostpi
Last active September 14, 2025 08:42
Show Gist options
  • Save anonhostpi/e33c2fb4e3282ff75962cf12a2a9af6a to your computer and use it in GitHub Desktop.
Save anonhostpi/e33c2fb4e3282ff75962cf12a2a9af6a to your computer and use it in GitHub Desktop.
How to do wasm in powershell
# 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