binaryen icon indicating copy to clipboard operation
binaryen copied to clipboard

Reference types crash wasm-opt‘s asyncify pass

Open surma opened this issue 3 years ago • 17 comments

(module
  (type (;0;) (func))
  (type (;1;) (func (result externref)))
  (import "gl" "createProgram" (func $src/asc/gl/createProgram (type 1)))
  (func $src/asc/lemonsoda/init (type 0)
    call $src/asc/gl/createProgram
    drop
  )
  (export "init" (func $src/asc/lemonsoda/init))
)
$ wat2wasm --enable-reference-types test.wat -o test.wasm
$ wasm-opt --asyncify --enable-reference-types test.wasm -o test2.wasm
invalid type
UNREACHABLE executed at /tmp/binaryen-20210320-71286-1g5tfwr/binaryen-version_100/src/wasm/wasm-type.cpp:488!
[1]    56081 abort      wasm-opt --asyncify --enable-reference-types test.wasm -o test2.wasm

surma avatar Mar 25 '21 23:03 surma

It seems to be specifically about using externref in imported functions. Both an externref parameter and return type will trigger this error, but not for an exported function.

surma avatar Mar 26 '21 00:03 surma

FWIW, this is not limited to externref, it happens for all reference types since they can't be stored on memory. Just out of curiosity, @kripken would this entail setting the reftype value on a table and store the index instead?

martianboy avatar Mar 26 '21 09:03 martianboy

@martianboy Ah of course! That makes a lot of sense.

Since the reference types proposal also allows having multiple tables, it seems like your idea with the table is a really good one.

surma avatar Mar 26 '21 11:03 surma

@surma Right, although this cannot happen yet in Binaryen, as it still doesn't have table operations and typed tables. But it shouldn't be too difficult to support reference types in Asyncify after those are done. I may still be missing something though.

martianboy avatar Mar 26 '21 13:03 martianboy

I think that's correct @martianboy & @surma. Yes, the issue is writing references to linear memory. Instead we would need to create a new table in asyncify, and use that to spill values using table.get/table.set, etc., so as mentioned above we'd need support for those features.

kripken avatar Mar 26 '21 15:03 kripken

Thanks for confirmation, @kripken. I'm in the middle of adding types to tables and element segments and after that I was planning to add table operations. I can see if I can fix Asyncify next. :)

martianboy avatar Mar 26 '21 16:03 martianboy

Sounds great @martianboy ! I hope that's pretty straightforward when you get to it (only nontrivial part might be to have two "stack pointers"), let me know if you have questions I can help with.

kripken avatar Mar 26 '21 17:03 kripken

Not to be that impatient dude on GH, but curious if there are any updates on this?

surma avatar Apr 28 '21 17:04 surma

@surma So I'm "almost" done with implementation of table instructions which was a prerequisite. My guess is that I will be able to get to Asyncify in about 10 days. Sorry for the wait. :)

martianboy avatar Apr 28 '21 18:04 martianboy

Oh please, don’t apologize! I’m just curious! Very excited to see this :)

surma avatar Apr 28 '21 21:04 surma

Hey @martianboy, just wanted to gently ask if you had any time to get to this? 🙇‍♂️

surma avatar Aug 27 '21 08:08 surma

@kripken looks like the problem is still exists, can we elaborate a little bit more on how it should be solved, I'd be happy to spend time for this. any real blockers, or just a matter of implementation?

alexdoesh avatar May 27 '22 22:05 alexdoesh

@alexdoesh I think we know how it should work, looks like a summary is in

https://github.com/WebAssembly/binaryen/issues/3739#issuecomment-808330306

In more detail, we spill stuff to a buffer in linear memory normally, in the existing code. For reference types we need a buffer in a table that we can write references to. So we'd need to add such a table to the wasm, and add some code that manages allocating buffers in it, and then add code to allocate the buffer and read/write from it (that is, if a local is a reference, we'd write to the table instead of linear memory).

If you want to do it that would be great!

kripken avatar May 27 '22 23:05 kripken

@kripken I've drafted a very direct/naive approach https://github.com/nxpub/binaryen/commit/449dd409d856311ecb9b68763de379e253c43d45 (will make a PR when have a proper draft) just to verify the logic. I'll rewrite the way table(s) is generated, will add support for other kind of refs and polish it properly.

Did I get the idea right, or I misunderstood the approach in general?

The sample from the first comment:

(module
  (type (;0;) (func))
  (type (;1;) (func (result externref)))
  (import "gl" "createProgram" (func $src/asc/gl/createProgram (type 1)))
  (func $src/asc/lemonsoda/init (type 0)
    call $src/asc/gl/createProgram
    drop
  )
  (export "init" (func $src/asc/lemonsoda/init))
)

generates:

(module
  (type (;0;) (func))
  (type (;1;) (func (param i32)))
  (type (;2;) (func (result externref)))
  (type (;3;) (func (result i32)))
  (import "gl" "createProgram" (func (;0;) (type 2)))
  (func (;1;) (type 0)
    (local externref externref i32 i32 i32 i32)
    global.get 0
    i32.const 2
    i32.eq
    if  ;; label = @1
      nop
      global.get 1
      i32.load
      local.set 4
      i32.const 0
      table.get 0
      local.set 0
    end
    block (result i32)  ;; label = @1
      block  ;; label = @2
        block  ;; label = @3
          global.get 0
          i32.const 2
          i32.eq
          if  ;; label = @4
            global.get 1
            global.get 1
            i32.load
            i32.const -4
            i32.add
            i32.store
            global.get 1
            i32.load
            i32.load
            local.set 3
          end
          block  ;; label = @4
            global.get 0
            i32.const 0
            i32.eq
            if (result i32)  ;; label = @5
              i32.const 1
            else
              local.get 3
              i32.const 0
              i32.eq
            end
            if  ;; label = @5
              call 0
              local.set 1
              global.get 0
              i32.const 1
              i32.eq
              if  ;; label = @6
                i32.const 0
                br 5 (;@1;)
              else
                local.get 1
                local.set 0
              end
            end
            global.get 0
            i32.const 0
            i32.eq
            if  ;; label = @5
              local.get 0
              drop
            end
          end
        end
        return
      end
      unreachable
    end
    local.set 2
    block  ;; label = @1
      global.get 1
      i32.load
      local.get 2
      i32.store
      global.get 1
      global.get 1
      i32.load
      i32.const 4
      i32.add
      i32.store
    end
    block  ;; label = @1
      global.get 1
      i32.load
      local.set 5
      i32.const 0
      local.get 0
      table.set 0
      nop
    end)
  (func (;2;) (type 1) (param i32)
    i32.const 1
    global.set 0
    local.get 0
    global.set 1
    global.get 1
    i32.load
    global.get 1
    i32.load offset=4
    i32.gt_u
    if  ;; label = @1
      unreachable
    end)
  (func (;3;) (type 0)
    i32.const 0
    global.set 0
    global.get 1
    i32.load
    global.get 1
    i32.load offset=4
    i32.gt_u
    if  ;; label = @1
      unreachable
    end)
  (func (;4;) (type 1) (param i32)
    i32.const 2
    global.set 0
    local.get 0
    global.set 1
    global.get 1
    i32.load
    global.get 1
    i32.load offset=4
    i32.gt_u
    if  ;; label = @1
      unreachable
    end)
  (func (;5;) (type 0)
    i32.const 0
    global.set 0
    global.get 1
    i32.load
    global.get 1
    i32.load offset=4
    i32.gt_u
    if  ;; label = @1
      unreachable
    end)
  (func (;6;) (type 3) (result i32)
    global.get 0)
  (table (;0;) 0 0 externref)
  (memory (;0;) 1 1)
  (global (;0;) (mut i32) (i32.const 0))
  (global (;1;) (mut i32) (i32.const 0))
  (export "init" (func 1))
  (export "asyncify_start_unwind" (func 2))
  (export "asyncify_stop_unwind" (func 3))
  (export "asyncify_start_rewind" (func 4))
  (export "asyncify_stop_rewind" (func 5))
  (export "asyncify_get_state" (func 6)))

alexdoesh avatar May 31 '22 09:05 alexdoesh

@alexdoesh

Yes, that looks like the right idea!

Aside from polish and the other stuff you already mentioned, the only other thing is I wonder if we want to support more than one pause at a time. We do that currently, and in Emscripten the Fibers API uses it. To support that here, we couldn't use absolute offsets in the table, but would need to allocate regions in it, which would add significant complexity. Also I think atm the user allocates the main buffer in linear memory, but here we would need to do it in Asyncify (since the user isn't even aware of the new table added here). It's probably best to not do that for now, but we should document the limitation, and perhaps there's a way we can assert at runtime.

kripken avatar May 31 '22 23:05 kripken

@kripken I had the feeling I'm not handling it properly, makes more sense now, thank you.

As I understand, we already have a helper to diagnose how many local variables (potentially affected by stack rewind) we have, I wanted to use it (or similar approach) anyway, it would allow us to understand the size of the region + what tables we would need (per ref-type) + some logic to "isolate" region per function. Does it seem enough? Or I misunderstood the meaning of "regions" in the ctx?

Do you think it's ok for asyncify pass to create a table if this would be explicit to the end user? Using a command line argument, for example (--reference-types-support or smth like this, and/or ability to provide a tables name prefix).

I'm not a C++ dev (I'm writing python compiler to wasm in python, inspired by asc), but still want to tackle the problem (if somebody would be patient to assist/review ofc), it looks pretty important at least for my pet project.

alexdoesh avatar Jun 03 '22 16:06 alexdoesh

@alexdoesh Yes, I think the current approach is enough for now. Just a comment mentioning the multi-pause issue sounds good.

Yes, I think it's the right thing to create a table here. We can do that automatically/silently in this pass. That should not be observable to the user (at least not unless they read the wasm :smile: ) since the table is not exported or imported.

I think with some cleanup that code can land. Good work!

kripken avatar Jun 03 '22 19:06 kripken

Any updates on this? @alexdoesh i tried your fork but it doesnt seem to work? Am I missing something here?

K0IN avatar Oct 15 '22 19:10 K0IN