Skip to content

Instantly share code, notes, and snippets.

@jamesu
Last active September 22, 2025 21:26
Show Gist options
  • Save jamesu/f0f5626b3edbc34dc728dfe262484200 to your computer and use it in GitHub Desktop.
Save jamesu/f0f5626b3edbc34dc728dfe262484200 to your computer and use it in GitHub Desktop.
(module
;; --- Import from host: print(offset: i32) -> void
(import "env" "print" (func $print (param i32)))
;; --- Linear memory: fixed at 2 pages (128 KiB) ---
(memory (export "memory") 2 2)
;; --- Data segment: static string ---
(data (i32.const 16) "Hello World\00")
;; --- Heap layout & globals ---
(global $heap_base i32 (i32.const 1024))
(global $heap_end i32 (i32.const 131072)) ;; 2 * 64 KiB
(global $heap_ptr (mut i32) (i32.const 1024))
(global $free_head (mut i32) (i32.const 0))
;; --- Helpers ---
(func $align_8 (param $n i32) (result i32)
local.get $n
i32.const 7
i32.add
i32.const -8
i32.and)
(func $push_free (param $blk i32)
local.get $blk
global.get $free_head
i32.store offset=4
local.get $blk
global.set $free_head)
(func $unlink (param $prev i32) (param $cur i32)
(local $next i32)
local.get $cur
i32.load offset=4
local.set $next
local.get $prev
i32.eqz
if
local.get $next
global.set $free_head
else
local.get $prev
local.get $next
i32.store offset=4
end)
;; --- Arithmetic ---
(func (export "add") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.add)
(func (export "sub") (param $a i32) (param $b i32) (result i32)
local.get $a
local.get $b
i32.sub)
;; --- malloc ---
(func (export "malloc") (param $n i32) (result i32)
(local $need i32) (local $total i32) (local $prev i32) (local $cur i32)
(local $sz i32) (local $hp i32) (local $newhp i32) (local $rem i32) (local $rem_blk i32)
;; handle n <= 0
local.get $n
i32.const 0
i32.le_s
if
i32.const 0
return
end
;; sizes
local.get $n
call $align_8
local.set $need
local.get $need
i32.const 8
i32.add
local.set $total
;; Wrap search in an outer block so we can br to the bump path.
block $bump_alloc
;; init free list scan
i32.const 0
local.set $prev
global.get $free_head
local.set $cur
loop $search
;; if cur == 0 → jump to bump_alloc
local.get $cur
i32.eqz
if
br $bump_alloc
end
;; load size of current block
local.get $cur
i32.load
local.set $sz
;; fit?
local.get $sz
local.get $total
i32.ge_u
if
;; unlink from free list
local.get $prev
local.get $cur
call $unlink
;; compute remainder
local.get $sz
local.get $total
i32.sub
local.set $rem
;; split if remainder >= 16 (header+min payload)
local.get $rem
i32.const 16
i32.ge_u
if
local.get $cur
local.get $total
i32.add
local.set $rem_blk
local.get $rem_blk
local.get $rem
i32.store
local.get $rem_blk
call $push_free
local.get $cur
local.get $total
i32.store
else
local.get $cur
local.get $sz
i32.store
end
;; return payload
local.get $cur
i32.const 8
i32.add
return
end
;; advance
local.get $cur
local.set $prev
local.get $cur
i32.load offset=4
local.set $cur
br $search
end
;; --- bump allocation path (runs when we br $bump_alloc) ---
global.get $heap_ptr
local.set $hp
local.get $hp
local.get $total
i32.add
local.set $newhp
local.get $newhp
global.get $heap_end
i32.gt_u
if
i32.const 0
return
end
local.get $hp
local.get $total
i32.store
local.get $newhp
global.set $heap_ptr
local.get $hp
i32.const 8
i32.add
return
end ;; end block $bump_alloc
;; We should never fall through here, but satisfy the validator:
i32.const 0
return
)
;; --- free ---
(func (export "free") (param $p i32)
(local $blk i32)
local.get $p
i32.eqz
if
return
end
local.get $p
i32.const 8
i32.sub
local.set $blk
local.get $blk
call $push_free)
;; --- testPrint ---
(func (export "testPrint")
i32.const 16 ;; offset of "Hello World"
call $print)
)
#include "console/console.h"
#include "console/consoleTypes.h"
#include "sim/simBase.h"
#include "core/fileStream.h"
#include "wasm3.h"
/*
KorkScript WASM module binder.
Module exports get exposed as namespace functions.
Host exports are namespace functions which can be imported by wasm modules.
Example usage:
new WasmModuleObject(MyModule)
{
// Funcs in wasm
funcName[0] = "add";
funcSig[0] = "i(ii)";
funcName[0] = "sub";
funcSig[1] = "i(ii)";
// Host funcs
hostFuncName[0] = "print";
hostFuncSig[1] = "v(s)";
moduleFile = "test.wasm";
};
function MyModule::print(%this, %msg)
{
echo("Module Print: " @ %msg);
}
echo(MyModule.add(1,2));
*/
class WasmModuleObject : public SimObject
{
typedef SimObject Parent;
struct FuncInfo
{
StringTableEntry localName;
IM3Function func;
};
enum
{
MAX_FUNCS = 16
};
struct FuncUserInfo
{
WasmModuleObject* module;
U32 funcIdx;
};
struct ScratchAlloc
{
U8* ptr;
U32 vmOffset;
};
public:
DECLARE_CONOBJECT(WasmModuleObject);
IM3Runtime mRuntime;
IM3Environment mEnv;
IM3Module mModule;
U32 mMemSize;
U32 mScratchSize;
StringTableEntry mModuleFile;
// These get called inside WASM
StringTableEntry mFuncNames[MAX_FUNCS];
StringTableEntry mFuncSignatures[MAX_FUNCS];
IM3Function mFuncs[MAX_FUNCS];
FuncUserInfo mInfos[MAX_FUNCS];
// These get called from WASM thunk
StringTableEntry mHostFuncs[MAX_FUNCS];
StringTableEntry mHostFuncSignatures[MAX_FUNCS];
FuncUserInfo mHostInfos[MAX_FUNCS];
//StringTableEntry mClassName;
U32 mScratchOffset;
U32 mScratchAllocPtr;
static void initPersistFields()
{
Parent::initPersistFields();
addField("memSize", TypeS32, Offset(mMemSize, WasmModuleObject));
addField("moduleFile", TypeString, Offset(mModuleFile, WasmModuleObject));
addField("funcName", TypeString, Offset(mFuncNames[2], WasmModuleObject), MAX_FUNCS-2);
addField("funcSig", TypeString, Offset(mFuncSignatures[2], WasmModuleObject), MAX_FUNCS-2);
addField("hostFuncName", TypeString, Offset(mHostFuncs, WasmModuleObject), MAX_FUNCS);
addField("hostFuncSig", TypeString, Offset(mHostFuncSignatures, WasmModuleObject), MAX_FUNCS);
// KorkScript-specific
addField("className", TypeString, Offset(mClassName, WasmModuleObject));
}
WasmModuleObject()
{
mRuntime = NULL;
mEnv = NULL;
mModule = NULL;
mMemSize = 128 * 1024;
mScratchSize = 256;
mModuleFile = NULL;
mClassName = NULL;
memset(mFuncNames, '\0', sizeof(mFuncNames));
memset(mFuncSignatures, '\0', sizeof(mFuncSignatures));
memset(mFuncs, '\0', sizeof(mFuncs));
memset(mInfos, '\0', sizeof(mInfos));
memset(mHostFuncs, '\0', sizeof(mHostFuncs));
memset(mHostFuncSignatures, '\0', sizeof(mHostFuncSignatures));
memset(mHostInfos, '\0', sizeof(mHostInfos));
mScratchOffset = 0;
mScratchAllocPtr = 0;
}
~WasmModuleObject()
{
cleanup();
}
void initScratch()
{
// Make sure scratch space is allocated
if (mScratchOffset == 0)
{
if (mFuncs[0])
{
// Call allocator
mScratchOffset = 0;
}
}
}
ScratchAlloc allocScratch(U32 bytes)
{
ScratchAlloc alloc = {};
U32 memSize = 0;
U8* mem = ((U8*) m3_GetMemory(mRuntime, &memSize, 0)) + mScratchAllocPtr;
alloc.vmOffset = mScratchAllocPtr;
mScratchAllocPtr += bytes;
if (mScratchAllocPtr > mScratchSize)
{
alloc.vmOffset = 0;
return alloc;
}
alloc.ptr = mem;
return alloc;
}
void resetScratch()
{
mScratchAllocPtr = 0;
}
void cleanup()
{
if (!mRuntime)
{
return;
}
m3_FreeRuntime(mRuntime);
m3_FreeEnvironment(mEnv);
mRuntime = NULL;
mEnv = NULL;
}
bool onAdd()
{
mNSLinkMask = LinkClassName;
if (!Parent::onAdd())
return false;
FileStream fs;
// Use malloc and free for base allocators
mFuncNames[0] = StringTable->insert("malloc");
mFuncNames[1] = StringTable->insert("free");
mFuncSignatures[0] = StringTable->insert("i(i)");
mFuncSignatures[1] = StringTable->insert("v(i)");
mEnv = m3_NewEnvironment();
mRuntime = m3_NewRuntime(mEnv, mMemSize, NULL);
if (!mRuntime || !fs.open(mModuleFile, FileStream::Read))
{
cleanup();
return false;
}
if (!load(fs) || !linkFuncs())
{
cleanup();
return false;
}
return true;
}
bool load(Stream& s)
{
U32 sz = s.getStreamSize();
U8* bytes = new U8[sz];
s.read(sz, bytes);
M3Result res = m3_ParseModule(mEnv, &mModule, bytes, sz);
if (res == NULL)
{
res = m3_LoadModule(mRuntime, mModule);
}
return res == NULL;
}
void convertSig(char* sig)
{
while (*sig != '\0')
{
if (*sig == 's')
*sig = 'i';
sig++;
}
}
bool isSigValid(const char* sig)
{
const char* startSig = sig;
while (*sig != '\0' && *sig != '(')
sig++;
if (*sig == '\0' || (sig - startSig != 1))
return false;
while (*sig != '\0' && *sig != ')')
sig++;
if (*sig != ')')
return false;
if (sig - startSig > 31)
return false;
return true;
}
U32 getSigParamCount(const char* sig)
{
const char* startSig = sig;
while (*sig != '\0' && *sig != '(')
sig++;
if (*sig == '\0')
return 0;
startSig = sig = sig+1;
while (*sig != '\0' && *sig != ')')
sig++;
if (*sig != ')')
return 0;
return (U32)(sig - startSig);
}
bool linkFuncs()
{
KorkApi::Vm* theVm = getVM();
KorkApi::NamespaceId nsId = getNamespace();
// See if we have host funcs; register these with the VM (needs to be done FIRST)
for (U32 i=0; i<MAX_FUNCS; i++)
{
if (mHostFuncs[i] != NULL && mHostFuncs[i] != StringTable->EmptyString &&
mHostFuncSignatures[i] != NULL && mHostFuncSignatures[i] != StringTable->EmptyString &&
isSigValid(mHostFuncSignatures[i]))
{
char realSig[32];
FuncUserInfo* hostInfo = &mHostInfos[i];
strcpy(realSig, mHostFuncSignatures[i]);
convertSig(realSig);
hostInfo->module = this;
hostInfo->funcIdx = i;
M3Result res = m3_LinkRawFunctionEx(mModule,
"env",
mHostFuncs[i],
realSig,
thunkHostCall,
hostInfo);
if (res != NULL)
{
Con::warnf("Function %s %s not bound (%s)", mHostFuncs[i], realSig, res);
}
}
}
// Bind all funcs in nsId
for (U32 i=0; i<MAX_FUNCS; i++)
{
if (mFuncNames[i] != NULL && mFuncNames[i] != StringTable->EmptyString &&
mFuncSignatures[i] != NULL && mFuncSignatures[i] != StringTable->EmptyString &&
isSigValid(mFuncSignatures[i]))
{
FuncUserInfo* info = &mInfos[i];
info->module = this;
info->funcIdx = i;
U32 paramCount = getSigParamCount(mFuncSignatures[i]);
M3Result result = m3_FindFunction(&mFuncs[i], mRuntime, mFuncNames[i]);
if (result != NULL)
{
Con::warnf("Can't find function %s %s (%s)", mFuncNames[i], mFuncSignatures[i], result);
}
else
{
theVm->addNamespaceFunction(nsId, mFuncNames[i], (KorkApi::StringFuncCallback)thunkCall, info, mFuncSignatures[i], paramCount+2, paramCount+2);
}
}
}
initScratch();
return true;
}
// TS -> WASM Thunk
static const char* thunkCall(WasmModuleObject* obj, void* userPtr, int argc, char** argv)
{
FuncUserInfo* userInfo = (FuncUserInfo*)userPtr;
WasmModuleObject* userModule = userInfo->module;
KorkApi::Vm* vm = userModule->getVM();
StringTableEntry sig = userModule->mFuncSignatures[userInfo->funcIdx];
StringTableEntry fname = userModule->mFuncNames[userInfo->funcIdx];
IM3Function func = userModule->mFuncs[userInfo->funcIdx];
if (!sig || !fname || !func)
return "bad_sig_or_name";
// Parse signature: <ret>(<params>)
const char* s = sig;
char retCh = 'v';
if (*s && *s != '(') { retCh = *s; }
while (*s && *s != '(') ++s;
if (*s == '(') ++s;
// Prepare scratch buffer for strings
userModule->resetScratch();
void* wasmArgv[16];
U64 wasmArgData[16];
U32 paramIdx = 0;
argc -= 2;
argv += 2;
if (argc < 0 || argc != m3_GetArgCount(func) || argc > 16)
return "bad_argc";
for (U32 i=0; i<argc; i++)
{
char t = s[i];
wasmArgv[i] = &wasmArgData[i];
void* data = (void*)&wasmArgData[i];
if (t == 's')
{
// copy argv[paramIdx] into module memory, get offset, pass as decimal string
const char* src = argv[i] ? argv[i] : "";
U32 len = (U32)strlen(src);
ScratchAlloc alloc = userModule->allocScratch(len + 1);
if (!alloc.ptr) return "scratch_oom";
memcpy(alloc.ptr, src, len);
alloc.ptr[len] = '\0';
((U32*)wasmArgData)[i] = alloc.vmOffset;
}
else if (t == 'i')
{
*(S32*)(&wasmArgData[i]) = dAtoi(argv[i]);
}
else if (t == 'I')
{
*(S64*)(&wasmArgData[i]) = dAtoi(argv[i]);
}
else if (t == 'f')
{
*(F32*)(&wasmArgData[i]) = dAtof(argv[i]);
}
else if (t == 'F')
{
*(F64*)(&wasmArgData[i]) = dAtof(argv[i]);
}
else
{
((U32*)wasmArgData)[i] = 0;
}
}
// Call the wasm function
M3Result r = m3_Call(func, argc, (const void**)wasmArgv);
if (r) return r;
// Prepare a buffer for returning a string to TorqueScript
KorkApi::ConsoleValue outBufV = vm->getStringFuncBuffer(1024);
char* outBuf = (char*)outBufV.evaluatePtr(vm->getAllocBase());
if (!outBuf) return "no_vm_buffer";
// No return?
if (retCh == 'v' || m3_GetRetCount(func) == 0)
{
outBuf[0] = '\0';
return outBuf;
}
void* retPtr[1] = {};
if (retCh == 's') // string
{
uint32_t off = 0;
retPtr[0] = &off;
r = m3_GetResults(func, 1, (const void**)&retPtr);
if (r)
{
return r;
}
U32 memSize = 0;
U8* mem = (U8*)m3_GetMemory(userModule->mRuntime, &memSize, 0);
if (!mem || off >= memSize)
{
outBuf[0] = '\0';
return outBuf;
}
const char* strInMem = (const char*)(mem + off);
dStrncpy(outBuf, strInMem, 1024);
return outBuf;
}
else if (retCh == 'i')
{
S32 value = 0;
retPtr[0] = &value;
r = m3_GetResults(func, 1, (const void**)&retPtr);
if (r)
{
return r;
}
dSprintf(outBuf, 16, "%i", value);
return outBuf;
}
else if (retCh == 'I')
{
S64 value = 0;
retPtr[0] = &value;
r = m3_GetResults(func, 1, (const void**)&retPtr);
if (r)
{
return r;
}
dSprintf(outBuf, 16, "%ll", value);
return outBuf;
}
else if (retCh == 'f')
{
F32 value = 0;
retPtr[0] = &value;
r = m3_GetResults(func, 1, (const void**)&retPtr);
if (r)
{
return r;
}
dSprintf(outBuf, 32, "%.9g", value);
return outBuf;
}
else if (retCh == 'F')
{
F64 value = 0;
retPtr[0] = &value;
r = m3_GetResults(func, 1, (const void**)&retPtr);
if (r)
{
return r;
}
dSprintf(outBuf, 32, "%.17g", value);
return outBuf;
}
else
{
return "";
}
}
// WASM -> TS Thunk
// Converts input to const char*
static const void* thunkHostCall(IM3Runtime rt,
IM3ImportContext ctx,
uint64_t* sp, // value stack (args in 64-bit slots)
void* mem)
{
const FuncUserInfo* userInfo = (const FuncUserInfo*)ctx->userdata;
WasmModuleObject* userModule = userInfo->module;
StringTableEntry sig = userModule->mHostFuncSignatures[userInfo->funcIdx];
KorkApi::Vm* vm = userModule->getVM();
bool returnsString = *sig == 's';
bool returnsValue = *sig != 'v';
while (*sig != '\0' && *sig != '(')
sig++;
if (*sig == '(')
sig++;
// discover which import this is and its type
uint32_t argc = m3_GetArgCount(ctx->function);
const char* argv_local[16];
const char** thunk_argv = &argv_local[2];
KorkApi::ConsoleValue bufspaceV = vm->getStringFuncBuffer(1024);
char* bufspace = (char*)bufspaceV.evaluatePtr(vm->getAllocBase());
size_t ofs = 0;
const size_t capacity = 1024-1;
argv_local[0] = NULL;
argv_local[1] = NULL;
for (uint32_t i = 0; i < argc; ++i)
{
M3ValueType t = m3_GetArgType(ctx->function, i);
bool isStr = sig[i] == 's';
const char* s = "";
char* bufStart = &bufspace[ofs];
if (isStr && t == c_m3Type_i32)
{
// Resolve string pointer
U32 memSize = 0;
U8* mem = ((U8*) m3_GetMemory(userModule->mRuntime, &memSize, 0));
U32 offset = (U32)sp[i];
if (offset < memSize)
{
s = (const char*)mem + offset;
}
ofs += snprintf(bufStart, capacity-ofs, "%s", s);
}
else if (t == c_m3Type_i32) { int32_t v = (int32_t) sp[i]; ofs += snprintf(bufStart, sizeof(bufspace)-ofs, "%d", v); }
else if (t == c_m3Type_i64) { int64_t v = (int64_t) sp[i]; ofs += snprintf(bufStart, sizeof(bufspace)-ofs, "%lld",(long long)v); }
else if (t == c_m3Type_f32) { float v = *(float*) (&sp[i]); ofs += snprintf(bufStart, sizeof(bufspace)-ofs, "%.9g", v); }
else if (t == c_m3Type_f64) { double v = *(double*)(&sp[i]); ofs += snprintf(bufStart, sizeof(bufspace)-ofs, "%.17g",v); }
bufspace[ofs++] = '\0';
thunk_argv[i] = bufStart;
}
KorkApi::ConsoleValue retV;
vm->callObjectFunction(userModule->getVMObject(),
userModule->mHostFuncs[userInfo->funcIdx],
argc+2, &argv_local[0], retV);
// marshal result(s) back to wasm
// If callee expects a value, set it on the stack tail according to return type.
if (m3_GetRetCount(ctx->function) == 0)
{
return m3Err_none;
}
M3ValueType rt0 = m3_GetRetType(ctx->function, 0);
if (rt0 != c_m3Type_i32)
{
returnsString = false;
}
if (returnsString)
{
// Prepare scratch buffer for strings
userModule->resetScratch();
// Convert to string
const char* strValue = vm->valueAsString(retV);
U32 size = strlen(strValue);
ScratchAlloc alloc = userModule->allocScratch(size);
if (alloc.ptr)
{
memcpy(alloc.ptr, strValue, size);
alloc.ptr[size] = '\0';
}
*(uint32_t*)sp = alloc.vmOffset;
}
else
{
if (rt0 == c_m3Type_i32) {
*(int32_t*)sp = vm->valueAsInt(retV);
} else if (rt0 == c_m3Type_i64) {
*(int64_t*)sp = vm->valueAsInt(retV);
} else if (rt0 == c_m3Type_f32) {
*(F32*)sp = vm->valueAsFloat(retV);
} else if (rt0 == c_m3Type_f64) {
*(F64*)sp = vm->valueAsFloat(retV);
} else {
return "m3Err_trapReturnType";
}
}
return m3Err_none;
}
};
IMPLEMENT_CONOBJECT(WasmModuleObject);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment