04:00
<Mathieu Hofman>
Mathieu Hofman: here's a hypothetical when you're back. would making shared structs inaccessible outside of shared { } code blocks (a la unsafe { } blocks in rust) be considered sufficient syntactic friction?
what do you mean by making inaccessible? I doubt you mean preventing interaction with instances of share structs outside these blocks? I don't see how could even work.
04:07
<Mathieu Hofman>
This is a pretty broad thing to be skeptical of. How does this fit together with rbuckton's feedback that methods were important for usability? Also, are you considering that the fundamental technology ("TLS") is needed for Wasm anyway, so most of the complexity will be there in the system either way?
For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there.
04:14
<Mathieu Hofman>
even though FinalizationRegistry uses a similarly function/constructor-based API with no syntax, that doesn't really provide any meaningful friction to prevent abuse. The motivation for abuse doesn't come from convenient syntax but rather useful semantics that people misunderstand and want to get at.
FinalizationRegistry is different enough from destructors that it forces you to rethink what you're actually doing. Of course that doesn't guarantee the author will get it right.
Shared structs is an improvement over SAB for complex value types. However SAB did force you to think about what you were doing when coming from an object model. The concern here is that an author can too easily take a regular non shared aware class, and transform it into a shared struct, without really thinking about what they're doing.
11:58
<littledan>
For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there.
The prototype-attaching thing could be done by Proxy, if you have the TLS primitive. That is, it can be implemented just with what Wasm will already add.
12:58
<rbuckton>
For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there.
For WASM shared objects to be remotely usable from JS, you need to be able to interact with them somehow. If they are opaque, that only means that interactions must go through a wrapper/Proxy, as littledan said, and also likely need to be valid WeakMap keys so that such proxies work. As a result, opaque WASM shared objects are not inherently safer, just slower due to indirection and FFI marshaling. When I brought up having WASM shared objects be opaque entities in a prior structs meeting, the main purpose was to discuss a worst case fallback position if we don't have a comprehensive story for JS shared objects. If WASM shared objects were to be introduced as ordinary JS objects and we were to want to later introduce JS shared structs with unique semantics around field reads and writes (such as what I discussed above re unsafe), then mutable WASM shared objects couldn't align with that approach without breaking existing consumers. Whether WASM shared objects are opaque or not has nothing to do with thread safety, only runtime semantic consistency. Thread safety is still a split responsibility between the shared object implementer and shared object consumer based on the needs of any given use case.
14:08
<shu>
For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there.
attaching prototypes is coming eventually, it's just not prioritized ahead of shared wasmgc
14:09
<shu>
what do you mean by making inaccessible? I doubt you mean preventing interaction with instances of share structs outside these blocks? I don't see how could even work.
i do mean that. like, imagine all the vtable methods like [[GetOwnProperty]] throw if you're not inside one of these blocks
14:09
<shu>
i said hypothetical
14:09
<shu>
suspend your disbelief
14:09
<shu>
if it's possible, is that considered "enough friction"
14:13
<shu>
here's what i want to do: i'd like to get your side to articulate a greatest lower bound on what's "enough friction", then we analyze why that's considered enough. if there's a design principle there that's not "because we feel like it is", then happy to continue the discussion, otherwise not productive
14:17
<shu>
For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there.
anyway the real answer is that if the prototype semantics as proposed here isn't part of this proposal, it'll be done as part of the wasm/js API because we still believe that's the best semantics to bridge the shared/unshared worlds
14:28
<rbuckton>

If you wanted, for example, to implement something akin to Rust's Mutex<T>, you could do so with a Proxy whether it's an opaque WASM shared object or a JS native shared struct. Assuming we could have methods and private state in a JS struct, you could accomplish something similar to this example in the Rust docs (NOTE: uses module expressions):

// main.js
import { Thread } from "./thread.js";
import { MutexValue } from "./mutex_value.js";

function main() {
  const counter = new MutexValue(0);
  const handles = [];

  for (let i = 0; i < 10; i++) {
    const handle = new Thread(module {
      import "./mutex_value.js"; // correlates prototype for MutexValue

      export function threadStart(counter) {
        using lck = counter.lock();
        const num = lck.unwrap();

        num.value += 1;
      }

    }, counter);

    handles.push(handle);
  }

  for (const handle of handles) {
    handle.join();
  }

  using lck = counter.lock();
  const num = lck.unwrap();
  console.log(`Result: ${num.value}`);
}

// thread.js
shared struct ThreadState {
  #mut = new Mutex();
  #cv = new Condition();
  #exited = false;

  exit() unsafe {
    using void = new UniqueLock(this.#mut);
    this.#exited = true;
    this.#cv.notify();
  }

  join() unsafe {
    if (this.#exited) return;
    using lck = new UniqueLock(this.#mut);
    this.#cv.wait(lck, () => this.#exited);
  }
}

export class Thread {
  #state;
  #worker;
  constructor(body, threadData) {
    this.#state = new ThreadState();
    this.#worker = new Worker(module {
      import "./thread.js"; // correlates prototype for ThreadState
      import { workerData } from "node:worker_threads";
      import { threadStart } from body;

      const [threadState, threadData] = workerData;
      try {
        threadStart(threadData);
      }
      finally {
        threadState.exit();
      }
    }, { workerData: [this.#state, threadData] });
  }

  join() {
    this.#state.join();
  }
}

// mutex_value.js
export shared struct MutexValue {
  static #Lock = class {
    #stack;
    #ref;

    constructor(owner) unsafe {
      using stack = new DisposableStack();
      stack.use(new UniqueLock(owner.#mutex));
      const { proxy, revoke } = Proxy.revocable({
        get value() { return owner.#value; },
        set value(v) { owner.#value = v; },
      });
      stack.defer(revoke);
      this.#ref = proxy;
      this.#stack = stack.move();
    }

    unwrap() {
      if (this.#stack.disposed) throw new ReferenceError();
      return this.#ref;
    }

    [Symbol.dispose]() {
      using _ = this.#stack;
    }
  };

  #mutex = new Mutex();
  #value;

  constructor(value) {
    this.#value = value;
  }

  lock() unsafe {
    return new MutexValue.#Lock(this);
  }
}
17:14
<littledan>
It's quite frustrating and feels counterproductive that, to pursue features like this, we have to resort to these frequent "threats" that it will come anyway. I wish we could focus on how the design should go, rather than whether it should be cancelled.
17:16
<littledan>
This style of discourse is a barrier to inclusion. E.g., for AsyncContext, it took years until people joined Chengzhong to talk through why it wasn't a fatally bad idea with respect to SES ideals.
17:17
<littledan>
Many us are spending work time on these projects, and this sort of opposition also makes it more difficult to justify spending time on these investments.
17:18
<littledan>
Of course we shouldn't add everything to JS, but somehow we need to be able to open the discussion, talk it through, and draw a conclusion, rather than rehashing the same concerns for years.