JairusSW / as-json

The only JSON library you'll need for AssemblyScript. SIMD enabled
MIT License
80 stars 16 forks source link

partial initialisation of deserialised class object - RuntimeError: out of bounds memory access #87

Closed spino17 closed 1 month ago

spino17 commented 4 months ago

I am trying out below code which has some imports which gets length prefixed strings from memory. These host native imports implementation is closed. Below is the code


import { JSON } from "json-as/assembly";

// Runtime imported functions
// @ts-ignore
@external("env", "get_state")
declare function getState(): i32

// @ts-ignore
@external("env", "get_args")
declare function getArgs(): i32

// @ts-ignore
@external("env", "debug_log")
declare function debugLog(buf: ArrayBuffer): i32

// @ts-ignore
@external("env", "set_state")
declare function setState(buf: ArrayBuffer): i32

type GetFn = () => i32;

function readStringFromMemory(fn: GetFn): string {
    let ptr = fn()
    let len: i32 = load<u32>(ptr)
    let buffer = new Uint8Array(len);

    for (let i = 0; i < len; ++i) {
        buffer[i] = load<u8>(ptr + 4 + i);
    }

    let s = String.UTF8.decode(buffer.buffer);
    return s
}

function getLengthPrefixedString(s: string): ArrayBuffer {
    let stringBuf = Uint8Array.wrap(String.UTF8.encode(s))
    let newLen = stringBuf.byteLength
    let buffer = new ArrayBuffer(4 + newLen);
    let dataView = new DataView(buffer);

    dataView.setUint32(0, newLen, true);

    for (let i = 0; i < newLen; ++i) {
        dataView.setInt8(4 + i, stringBuf[i])
    }

    return buffer
}

@json
class TokenMetaData {
  id: u64;
  name: string;
  uri: string;

  constructor(id: u64, name: string, uri: string) {
    this.id = id;
    this.name = name;
    this.uri = uri;
  }
}

@json
class NonFungibleToken {
  owner: string;
  counter: u64;
  tokens: Map<u64, TokenMetaData>;
  owners: Map<u64, string>;
  balances: Map<string, u64[]>;

  constructor() {
    this.owner = "";
    this.counter = 0;
    this.tokens = new Map();
    this.owners = new Map();
    this.balances = new Map();
  }

  mint(name: string, uri: string, toAddress: string): u64 {
    this.counter += 1;
    let id = this.counter;

    const tokenMetaData = new TokenMetaData(id, name, uri);

    // beyond this point program fails with `out of memory bound access` error!
    this.tokens.set(id, tokenMetaData);
    this.owners.set(id, toAddress);

    if (!this.balances.has(toAddress)) {
      this.balances.set(toAddress, []);
    }

    this.balances.get(toAddress).push(id);

    return id;
  }
}

// Argument classes
@json
class NonfungibletokenMintArgs {
    name!: string;
    uri!: string;
    toAddress!: string;
}

// WASM exported functions
export function init(): void {
    let serializedState = JSON.stringify<NonFungibleToken>(new NonFungibleToken());
    let buffer = getLengthPrefixedString(serializedState);
    setState(buffer);
}

export function mint(): void {
    let state_str = readStringFromMemory(getState);
    debugLog(getLengthPrefixedString("inside wasm module state: " + state_str)); // only for testing

    let args_str = readStringFromMemory(getArgs);
    debugLog(getLengthPrefixedString("inside wasm module args: " + args_str)); // only for testing

    let state = JSON.parse<NonFungibleToken>(state_str);
    let args = JSON.parse<NonfungibletokenMintArgs>(args_str);
    // let state = JSON.parse<NonFungibleToken>('{"owner":"","counter":0,"tokens":{},"owners":{},"balances":{}}');
    // let args = JSON.parse<NonfungibletokenMintArgs>('{"name":"yamini","toAddress":"awesome","uri":"bhavya"}');

    state.mint(args.name, args.uri, args.toAddress);

    let serializedState = JSON.stringify<NonFungibleToken>(state);
    let buffer = getLengthPrefixedString(serializedState);
    setState(buffer);
}

I am not understanding where the issue is, when I am using hardcoded strings (commented in above code), it is working fine, but when I am reading it from the memory and then parsing it, it is only able to initialise just the counter field rest the maps seem to be uninitialised. Is this issue related to json-as or assemblyscript garbage collection? I even tried compiler option --runtime stub but same error is occurring.

JairusSW commented 4 months ago

It looks like your writing directly to linear memory, right? I'm wondering if you are calling __new from the runtime to initialize a new string (or utf-8 encoded buffer in your case) If your not, any other allocation is going to overwrite that data and it can't be parsed correctly.

I optimized your code:

function readStringFromMemory(ptr: usize): string {
  const len = load<i32>(ptr);
  return String.UTF8.decodeUnsafe(ptr + 4, len);
}

function getLengthPrefixedString(s: string): ArrayBuffer {
  const newLen = String.UTF8.byteLength(s);
  const buffer = new ArrayBuffer(4 + newLen);
  store<i32>(changetype<usize>(buffer), newLen, 0);
  String.UTF8.encodeUnsafe(changetype<usize>(s), s.length, changetype<usize>(buffer) + 4);
  return buffer;
}
spino17 commented 4 months ago

Thanks @JairusSW for the response. No I am not adding any _new in the host runtime side while setting the memory! I saw the glue code generated in js and they don't use _new, they just set the memory appropriately and assemblyscript can interpret that as string or any other type. Can you please let me know where is the disconnect? I can be completely wrong with this thinking. And much thanks for the above optimized code 😄

JairusSW commented 4 months ago

What language is your runtime/app loader written in? Perhaps I can spin up an example

JairusSW commented 4 months ago

Also do you have WhatsApp or Discord? It might be easier to figure this out there

spino17 commented 4 months ago

Thanks @JairusSW for the help here, it's much needed and I highly appreciate. My runtime is in rust wasmer. My discord is spino17. Would love to connect with you over there and carry forward the discussion.