Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@
"engines": {
"node": ">=18"
}
}
}
298 changes: 162 additions & 136 deletions loader/src/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ const Emscripten2 = require('./formats/emscripten2.cjs')
const Emscripten3 = require('./formats/emscripten3.cjs')
const Emscripten4 = require('./formats/emscripten4.cjs')
const Wasm64 = require('./formats/wasm64-emscripten.cjs')
const metering = require('@permaweb/wasm-metering')
const metering = require('@permaweb/wasm-metering') // Re-enabled metering
// const metering = require('@permaweb/wasm-metering')

/* eslint-enable */

Expand Down Expand Up @@ -109,169 +110,194 @@ const metering = require('@permaweb/wasm-metering')
// Save the original WebAssembly.compile and compileStreaming functions
const originalCompileStreaming = WebAssembly.compileStreaming
const originalCompile = WebAssembly.compile

const shouldApplyMetering = (importObject = {}) => {
return ['wasm32-unknown-emscripten-metering', 'wasm64-unknown-emscripten-draft_2024_10_16-metering'].includes(importObject.format);
}
};

const applyMetering = (arrayBuffer, importObject) => {
const { format } = importObject
const { format } = importObject;

const view = ArrayBuffer.isView(arrayBuffer)
? arrayBuffer
: Buffer.from(arrayBuffer)
: Buffer.from(arrayBuffer);

// Determine meter type and apply metering
const meterType = format.startsWith('wasm32') ? 'i32' : 'i64'
const meteredView = metering.meterWASM(view, { meterType })
const meterType = format.startsWith('wasm32') ? 'i32' : 'i64';
const meteredView = metering.meterWASM(view, { meterType });

return originalCompile(meteredView.buffer)
}
return originalCompile(meteredView.buffer); // Use originalCompile (WebAssembly.compile)
};

// Override WebAssembly.compileStreaming to apply metering
// Override WebAssembly.compileStreaming to apply metering conditionally
WebAssembly.compileStreaming = async function (source, importObject = {}) {
if (!shouldApplyMetering(importObject)) return originalCompileStreaming(source)
if (!shouldApplyMetering(importObject)) return originalCompileStreaming(source);

const arrayBuffer = await source.arrayBuffer()
return applyMetering(arrayBuffer, importObject)
}
const arrayBuffer = await source.arrayBuffer();
return applyMetering(arrayBuffer, importObject);
};

// Override WebAssembly.compile to apply metering
// Override WebAssembly.compile to apply metering conditionally
WebAssembly.compile = async function (source, importObject = {}) {
if (!shouldApplyMetering(importObject)) return originalCompile(source)

return applyMetering(source, importObject)
}
if (!shouldApplyMetering(importObject)) return originalCompile(source);
return applyMetering(source, importObject);
};

module.exports = async function (binary, options) {
let instance = null
let doHandle = null
if (options === null || typeof options === 'undefined') {
options = { format: 'wasm32-unknown-emscripten' };
} else if (typeof options !== 'object') {
options = { format: 'wasm32-unknown-emscripten' };
}

let meterType = options.format.startsWith("wasm32") ? "i32" : "i64";
const unsafeMemory = options.UNSAFE_MEMORY === true;

try {
let instance = null;
let doHandle = null;
let meterType;

if (options === null) {
options = { format: 'wasm32-unknown-emscripten' }
}
if (options.format === "wasm32-unknown-emscripten") {
instance = await Emscripten(binary, options)
} else if (options.format === "wasm32-unknown-emscripten2") {
instance = await Emscripten2(binary, options)
} else if (options.format === "wasm32-unknown-emscripten3") {
instance = await Emscripten3(binary, options)
} else {

if (typeof binary === "function") {
// Determine meterType if a metering format is potentially used
const currentFormat = options.format || 'wasm32-unknown-emscripten';
if (shouldApplyMetering({ format: currentFormat })) {
meterType = currentFormat.startsWith("wasm32") ? "i32" : "i64";
}

if (options.format === "wasm32-unknown-emscripten") {
instance = await Emscripten(binary, options)
options.instantiateWasm = binary
} else if (options.format === "wasm32-unknown-emscripten2") {
instance = await Emscripten2(binary, options)
} else if (options.format === "wasm32-unknown-emscripten3") {
instance = await Emscripten3(binary, options)
} else {
if (options.format === "wasm32-unknown-emscripten-metering" || options.format === "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
binary = metering.meterWASM(binary, { meterType })
if (typeof binary === "function") {
options.instantiateWasm = binary
} else {
// Local metering for specific formats
if (options.format === "wasm32-unknown-emscripten-metering" || options.format === "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
if (metering && meterType) {
binary = metering.meterWASM(binary, { meterType });
}
}
options.wasmBinary = binary
}
options.wasmBinary = binary
}

if (options.format === "wasm64-unknown-emscripten-draft_2024_02_15" || options.format === "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
instance = await Wasm64(options)
} else if (options.format === "wasm32-unknown-emscripten4" || options.format === "wasm32-unknown-emscripten-metering") {
instance = await Emscripten4(options)
}

await instance['FS_createPath']('/', 'data')

doHandle = instance.cwrap('handle', 'string', ['string', 'string'], { async: true })
}

/**
* Since the module can be invoked multiple times, there isn't really
* a good place to cleanup these listeners (since emscripten doesn't do it),
* other than immediately.
*
* I don't really see where they are used, since CU implementations MUST
* catch reject Promises from the WASM module, as part of evaluation.
*
* TODO: maybe a better way to do this
*
* So we immediately remove any listeners added by Module,
* in order to prevent memory leaks
*/
if (instance.cleanupListeners) {
instance.cleanupListeners()
}

if (options.format !== "wasm64-unknown-emscripten-draft_2024_02_15" &&
options.format !== "wasm32-unknown-emscripten4" &&
options.format !== "wasm32-unknown-emscripten-metering" &&
options.format !== "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
doHandle = instance.cwrap('handle', 'string', ['string', 'string'])
}
if (options.format === "wasm64-unknown-emscripten-draft_2024_02_15" || options.format === "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
instance = await Wasm64(options)
} else if (options.format === "wasm32-unknown-emscripten4" || options.format === "wasm32-unknown-emscripten-metering") {
instance = await Emscripten4(options)
}

await instance['FS_createPath']('/', 'data')
doHandle = instance.cwrap('handle', 'string', ['string', 'string'], { async: true })
}

return async (buffer, msg, env) => {
/**
* instance is the Module
* abstraction for the WebAssembly environnment
*
* This is where things like module options, extensions
* and other metadata are exposed to things like WeaveDrive.
*
* So by setting blockHeight on the instance to the current
* msg's blockHeight, it can be made available to WeaveDrive
* for admissable checks
* Since the module can be invoked multiple times, there isn't really
* a good place to cleanup these listeners (since emscripten doesn't do it),
* other than immediately.
*
* I don't really see where they are used, since CU implementations MUST
* catch reject Promises from the WASM module, as part of evaluation.
*
* TODO: maybe a better way to do this
*
* So we immediately remove any listeners added by Module,
* in order to prevent memory leaks
*/
instance.blockHeight = msg['Block-Height']

const originalRandom = Math.random
// const OriginalDate = Date
const originalLog = console.log
try {
/** start mock Math.random */
Math.random = function () { return 0.5 }
/** end mock Math.random */

/** start mock console.log */
console.log = function () { return null }
/** end mock console.log */

if (buffer) {
if (instance.HEAPU8.byteLength < buffer.byteLength) {
console.log("RESIZE HEAP")
instance.resizeHeap(buffer.byteLength)
}
instance.HEAPU8.set(buffer)
if (instance.cleanupListeners) {
instance.cleanupListeners()
}

if (options.format !== "wasm64-unknown-emscripten-draft_2024_02_15" &&
options.format !== "wasm32-unknown-emscripten4" &&
options.format !== "wasm32-unknown-emscripten-metering" &&
options.format !== "wasm64-unknown-emscripten-draft_2024_10_16-metering") {
// Ensure doHandle is assigned if not in the async:true path from above
if (!doHandle) {
doHandle = instance.cwrap('handle', 'string', ['string', 'string'])
}
}

return async (buffer, msg, env) => {
/**
* Make sure to refill the gas tank for each invocation
* instance is the Module
* abstraction for the WebAssembly environnment
*
* This is where things like module options, extensions
* and other metadata are exposed to things like WeaveDrive.
*
* So by setting blockHeight on the instance to the current
* msg's blockHeight, it can be made available to WeaveDrive
* for admissable checks
*/
instance.gas.refill()

const res = await doHandle(JSON.stringify(msg), JSON.stringify(env))

const { ok, response } = JSON.parse(res)
if (!ok) throw response

/** unmock functions */
// eslint-disable-next-line no-global-assign
// Date = OriginalDate
Math.random = originalRandom
console.log = originalLog
/** end unmock */

return {
Memory: instance.HEAPU8.slice(),
Error: response.Error,
Output: response.Output,
Messages: response.Messages,
Spawns: response.Spawns,
Assignments: response.Assignments,
Patches: response.Patches || [],
GasUsed: instance.gas.used
}
} finally {
// eslint-disable-next-line no-global-assign
// Date = OriginalDate
Math.random = originalRandom
console.log = originalLog
buffer = null
}
}
}
instance.blockHeight = msg['Block-Height']

const originalRandom = Math.random
// const OriginalDate = Date
const originalLog = console.log
try {
/** start mock Math.random */
Math.random = function () { return 0.5 }
/** end mock Math.random */

/** start mock console.log */
console.log = function () { return null }
/** end mock console.log */

if (buffer) {
if (instance.HEAPU8.byteLength < buffer.byteLength) {
console.log("RESIZE HEAP")
instance.resizeHeap(buffer.byteLength)
}
instance.HEAPU8.set(buffer)
}
/**
* Make sure to refill the gas tank for each invocation
*/
if (instance.gas && typeof instance.gas.refill === 'function') {
instance.gas.refill();
}

const res = await doHandle(JSON.stringify(msg), JSON.stringify(env))

const { ok, response } = JSON.parse(res)
if (!ok) throw response

/** unmock functions */
// eslint-disable-next-line no-global-assign
// Date = OriginalDate
Math.random = originalRandom
console.log = originalLog
/** end unmock */

const result = {
Memory: unsafeMemory ? instance.HEAPU8.buffer : instance.HEAPU8.slice(), // Conditional memory return
Error: response.Error,
Output: response.Output,
Messages: response.Messages,
Spawns: response.Spawns,
Assignments: response.Assignments,
Patches: response.Patches || [],
};

// Add GasUsed if metering was applied
if (instance.gas && typeof instance.gas.used !== 'undefined') {
result.GasUsed = instance.gas.used;
}

/** unmock functions */
// eslint-disable-next-line no-global-assign
// Date = OriginalDate
Math.random = originalRandom
console.log = originalLog
/** end unmock */
buffer = null // Clear buffer reference

return result;
} // End of try block
finally { }
};
} // End of outer try block
finally { }
};
22 changes: 21 additions & 1 deletion loader/test/loader.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,40 @@ const testCases = [
binaryPath: './test/emscripten4/process.wasm', // Path to the 32-bit WASM binary
options: { format: 'wasm32-unknown-emscripten4' } // Format for 32-bit
},
{
name: 'Emscripten4 (32-bit) Unsafe',
binaryPath: './test/emscripten4/process.wasm',
options: { format: 'wasm32-unknown-emscripten4', UNSAFE_MEMORY: true }
},
{
name: 'Wasm64-Emscripten (64-bit)', // Test for the 64-bit WASM
binaryPath: './test/wasm64-emscripten/process.wasm', // Path to the 64-bit WASM binary
options: { format: 'wasm64-unknown-emscripten-draft_2024_02_15' } // Format for 64-bit
},
{
name: 'Wasm64-Emscripten (64-bit) Unsafe',
binaryPath: './test/wasm64-emscripten/process.wasm',
options: { format: 'wasm64-unknown-emscripten-draft_2024_02_15', UNSAFE_MEMORY: true }
},
{
name: 'Emscripten4 Metering (32-bit)', // Test for the 32-bit WASM
binaryPath: './test/emscripten4/process.wasm', // Path to the 32-bit WASM binary
options: { format: 'wasm32-unknown-emscripten-metering' } // Format for 32-bit metering
},
{
name: 'Emscripten4 Metering (32-bit) Unsafe',
binaryPath: './test/emscripten4/process.wasm',
options: { format: 'wasm32-unknown-emscripten-metering', UNSAFE_MEMORY: true }
},
{
name: 'Wasm64-Emscripten Metering (64-bit)', // Test for the 64-bit WASM
binaryPath: './test/wasm64-emscripten/process.wasm', // Path to the 64-bit WASM binary
options: { format: 'wasm64-unknown-emscripten-draft_2024_10_16-metering' } // Format for 64-bit metering
},
{
name: 'Wasm64-Emscripten Metering (64-bit) Unsafe',
binaryPath: './test/wasm64-emscripten/process.wasm',
options: { format: 'wasm64-unknown-emscripten-draft_2024_10_16-metering', UNSAFE_MEMORY: true }
}
]

Expand Down Expand Up @@ -79,7 +99,7 @@ describe('AoLoader Functionality Tests', () => {
it('load wasm and evaluate message', async () => {
const handle = await AoLoader(wasmBinary, options)
const mainResult = await handle(null, getMsg('return \'Hello World\''), getEnv())
//const mainResult = await handle(null, getMsg("print(os.getenv(\"PATH\"))"),getEnv())
// const mainResult = await handle(null, getMsg("print(os.getenv(\"PATH\"))"),getEnv())
// Check basic properties of the result
assert.ok(mainResult.Memory)
assert.ok(mainResult.hasOwnProperty('Messages'))
Expand Down