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
36 changes: 22 additions & 14 deletions assembly/api-debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,36 +169,44 @@ export function getPageDump(index: u32): Uint8Array {
}

/**
* Read a chunk of memory at `[address, address + length)`.
* Returns the WASM linear memory pointer (byte offset) for the backing buffer of the page at `page`.
*
* Returns the requested memory chunk or `null` if reading triggered a page fault.
* Returns `0` if the page does not exist or is not readable (page/access fault).
*
* @deprecated Getting memory like that is extremely inefficient (copying mulitple times)
* and error prone (we may not be able to allocate).
* Instead WASM should be able to return memory pointers for already allocated pages.
* So reading memory on the caller side should be something like this:
* Use this instead of `getMemory` to read memory efficiently from the JS side:
* ```ts
* let pagesRead = 0;
* for (let address = start; address < end; address += PAGE_SIZE) {
* const page = address >> PAGE_SIZE_SHIFT;
* const maybePointer = getPagePointer(page);
* // check page fault
* if (maybePointer === null) {
* const ptr = getPagePointer(page);
* if (ptr === 0) {
* throw new Error(`Page fault at ${page << PAGE_SIZE_SHIFT}`);
* }
* // otherwise copy to JS
* destination.set(
* new Uint8Array(wasm.instance.exports.memory.buffer, ptr, Math.min(end - address, PAGE_SIZE)),
* pagesRead << PAGE_SIZE_SHIFT,
* new Uint8Array(wasm.instance.memory, maybePointer, Math.min(end, PAGE_SIZE))
* );
* pagesRead += 1;
* }
* ```
*/
export function getPagePointer(page: u32): usize {
if (interpreter === null) {
return 0;
}
const int = <Interpreter>interpreter;
return int.memory.getPagePointer(page);
}

/**
* Read a chunk of memory at `[address, address + length)`.
*
* goals:
* 1. No additional allocations on the WASM side
* 2. Copying directly from wasm memory on the JS side
* Returns the requested memory chunk or `null` if reading triggered a page fault.
*
* @deprecated Getting memory like that is extremely inefficient (copying mulitple times)
* and error prone (we may not be able to allocate).
* Use `getPagePointer` instead to read memory directly from WASM linear memory on the JS side
* with no additional WASM-side allocations.
*/
export function getMemory(address: u32, length: u32): Uint8Array | null {
if (interpreter === null) {
Expand Down
34 changes: 33 additions & 1 deletion assembly/api-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ export function pvmSetRegisters(pvmId: u32, registers: u64[]): void {
/**
* Read a continuous chunk of memory from given PVM instance.
*
* @deprecated see getMemory for details
* @deprecated Use `pvmGetPagePointer` instead to read memory directly from WASM linear memory
* on the JS side with no additional WASM-side allocations.
*/
export function pvmReadMemory(pvmId: u32, address: u32, length: u32): Uint8Array | null {
if (pvms.has(pvmId)) {
Expand All @@ -167,6 +168,37 @@ export function pvmReadMemory(pvmId: u32, address: u32, length: u32): Uint8Array
return null;
}

/**
* Returns the WASM linear memory pointer (byte offset) for the backing buffer of the page at `page`
* in the given PVM instance.
*
* Returns `0` if the PVM does not exist, the page does not exist, or the page is not readable.
*
* Use this instead of `pvmReadMemory` to read memory efficiently from the JS side:
* ```ts
* let pagesRead = 0;
* for (let address = start; address < end; address += PAGE_SIZE) {
* const page = address >> PAGE_SIZE_SHIFT;
* const ptr = pvmGetPagePointer(pvmId, page);
* if (ptr === 0) {
* throw new Error(`Page fault at ${page << PAGE_SIZE_SHIFT}`);
* }
* destination.set(
* new Uint8Array(wasm.instance.exports.memory.buffer, ptr, Math.min(end - address, PAGE_SIZE)),
* pagesRead << PAGE_SIZE_SHIFT,
* );
* pagesRead += 1;
* }
* ```
*/
export function pvmGetPagePointer(pvmId: u32, page: u32): usize {
if (pvms.has(pvmId)) {
const int = pvms.get(pvmId);
return int.memory.getPagePointer(page);
}
return 0;
}

/** Write a chunk of memory to given PVM instance. */
export function pvmWriteMemory(pvmId: u32, address: u32, data: Uint8Array): boolean {
if (pvms.has(pvmId)) {
Expand Down
64 changes: 64 additions & 0 deletions assembly/memory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,68 @@ export const TESTS: Test[] = [

return assert;
}),
test("getPagePointer returns 0 for missing page", (assert) => {
const mem = new MemoryBuilder().build();
const pageIndex: u32 = RESERVED_PAGES + 5;

const ptr = mem.getPagePointer(pageIndex);

assert.isEqual(ptr, <usize>0, "should return 0 for non-existent page");
return assert;
}),
test("getPagePointer returns 0 for reserved (inaccessible) page", (assert) => {
const mem = new MemoryBuilder().build();

// Reserved pages (indices 0..RESERVED_PAGES-1) are never allocated, so pointer = 0.
const ptr = mem.getPagePointer(0);

assert.isEqual(ptr, <usize>0, "should return 0 for reserved page");
return assert;
}),
test("getPagePointer returns non-zero pointer for read-accessible page", (assert) => {
// Access.Write pages ARE readable (can(Access.Read) returns true for Write pages).
// Access.None pages are not. Create a Read-only page and verify pointer is non-zero.
const builder = new MemoryBuilder();
const pageAddress: u32 = RESERVED_MEMORY;
builder.setData(Access.Read, pageAddress, new Uint8Array(PAGE_SIZE));
const mem = builder.build();

const pageIndex: u32 = pageAddress >> PAGE_SIZE_SHIFT;
const ptr = mem.getPagePointer(pageIndex);

assert.isNotEqual(ptr, <usize>0, "should return non-zero pointer for readable page");
return assert;
}),
test("getPagePointer returns non-zero pointer for writable page", (assert) => {
const builder = new MemoryBuilder();
const pageAddress: u32 = RESERVED_MEMORY;
builder.setData(Access.Write, pageAddress, new Uint8Array(PAGE_SIZE));
const mem = builder.build();

const pageIndex: u32 = pageAddress >> PAGE_SIZE_SHIFT;
const ptr = mem.getPagePointer(pageIndex);

assert.isNotEqual(ptr, <usize>0, "should return non-zero pointer for writable page");
return assert;
}),
Comment thread
tomusdrw marked this conversation as resolved.
test("getPagePointer data matches page content", (assert) => {
const builder = new MemoryBuilder();
const pageAddress: u32 = RESERVED_MEMORY;
const data = new Uint8Array(PAGE_SIZE);
data[0] = 0xde;
data[1] = 0xad;
data[PAGE_SIZE - 1] = 0xff;
builder.setData(Access.Read, pageAddress, data);
const mem = builder.build();

const pageIndex: u32 = pageAddress >> PAGE_SIZE_SHIFT;
const ptr = mem.getPagePointer(pageIndex);

assert.isNotEqual(ptr, <usize>0, "pointer should be valid");
// Read bytes directly via the pointer and compare.
assert.isEqual(load<u8>(ptr + 0), <u8>0xde, "byte 0 should match");
Comment thread
tomusdrw marked this conversation as resolved.
assert.isEqual(load<u8>(ptr + 1), <u8>0xad, "byte 1 should match");
assert.isEqual(load<u8>(ptr + PAGE_SIZE - 1), <u8>0xff, "last byte should match");
return assert;
}),
];
34 changes: 34 additions & 0 deletions assembly/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,40 @@ export class Memory {
return this.pages.get(index).raw.data;
}

/**
* Returns the WASM linear memory pointer (byte offset) for the backing buffer of the page at `pageIndex`.
*
* Returns `0` if the page does not exist or is not readable (page/access fault).
*
* This enables efficient memory reading on the JS side without extra WASM allocations:
* ```ts
* let pagesRead = 0;
* for (let address = start; address < end; address += PAGE_SIZE) {
* const page = address >> PAGE_SIZE_SHIFT;
* const ptr = getPagePointer(page);
* if (ptr === 0) {
* throw new Error(`Page fault at ${page << PAGE_SIZE_SHIFT}`);
* }
* destination.set(
* new Uint8Array(wasm.instance.exports.memory.buffer, ptr, Math.min(end - address, PAGE_SIZE)),
* pagesRead << PAGE_SIZE_SHIFT,
* );
* pagesRead += 1;
* }
Comment thread
tomusdrw marked this conversation as resolved.
* ```
*/
getPagePointer(pageIndex: u32): usize {
if (!this.pages.has(pageIndex)) {
return 0;
}
const page = this.pages.get(pageIndex);
if (!page.can(Access.Read)) {
return 0;
}
// Trigger lazy allocation if the backing buffer has not been created yet.
return page.raw.data.dataStart;
}

free(): void {
const pages = this.pages.values();
for (let i = 0; i < pages.length; i++) {
Expand Down
8 changes: 8 additions & 0 deletions assembly/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ export class Assert {
this.errors.push(`Got: '${actualDisplay}', expected: '${expectDisplay}' @ ${msg}`);
}
}

isNotEqual<T>(actual: T, unexpected: T, msg: string = ""): void {
if (actual === unexpected) {
this.isOkay = false;
const actualDisplay = isInteger(actual) ? `${actual} (0x${actual.toString(16)})` : `${actual}`;
this.errors.push(`Expected value to differ from: '${actualDisplay}' @ ${msg}`);
}
}
}

export function test(name: string, ptr: (assert: Assert) => Assert): Test {
Expand Down