Bug in Embedded/ARC/WebAssembly combination?

I'm currently trying out the Swift on WebAssembly and the new Embedded feature, after reading Some feedback from my short experience with SwiftWasm - #5 by Max_Desiatov and Embedded Swift.

However, I'm seeing some strange behaviour when using classes/ARC: In some cases calls to deinit seem to be missing, and in some cases, deinit seems to be called before a last use of the instance.

I read ARC deinit time question and watched ARC in Swift: Basics and beyond - WWDC21 - Videos - Apple Developer, but I'm still confused as to how to explain this behaviour - I understand that the exact time of the release might be after the minimum of the lifetime, and that the order of releases is not defined, but the behaviour I mentioned above seems like a bug?

I tried to create small reproducers. I'm using Swift version 6.0-dev (LLVM cef183591317ec7, Swift 66e311074bff491) / swift-DEVELOPMENT-SNAPSHOT-2024-05-15-a-ubuntu22.04, and the following flags in the Package.swift file:

            cSettings: [
                .unsafeFlags(["-fdeclspec"])
            ],
            swiftSettings: [
                .enableExperimentalFeature("Embedded"),
                .interoperabilityMode(.C),
                .unsafeFlags([
                    "-wmo",
                    "-disable-cmo",
                    "-Xfrontend", "-disable-stack-protector"
                ])
            ],
            linkerSettings: [
                .unsafeFlags([
                    "-Xclang-linker", "-nostdlib",
                    "-Xlinker", "--no-entry"
                ])
            ]

The first reproducer:

@_extern(wasm, module: "foo", name: "a")
@_extern(c)
func foo_a()

@_extern(wasm, module: "foo", name: "b")
@_extern(c)
func foo_b()

@_extern(wasm, module: "foo", name: "c")
@_extern(c)
func foo_c()

class Foo {

    init() {
        foo_a()
    }

    func foo() {
        foo_b()
    }

    deinit {
        foo_c()
    }
}


@_expose(wasm, "test1")
@_cdecl("test1")
func test1() {
    let foo = Foo()
    foo.foo()
}

gets compiled to (pseudo-code produced by wasm-decompile):

export function test1() {
  foo_a();
  foo_b();
}

Why is the call to foo_c in deinit missing?

The second reproducer:

@_extern(wasm, module: "bar", name: "new")
@_extern(c)
func bar_new() -> UnsafeMutablePointer<Int>

@_extern(wasm, module: "bar", name: "use")
@_extern(c)
func bar_use(_ ptr: UnsafeMutablePointer<Int>)

@_extern(wasm, module: "bar", name: "free")
@_extern(c)
func bar_free(_ ptr: UnsafeMutablePointer<Int>)

class Bar {

    let ptr: UnsafeMutablePointer<Int>

    init() {
        ptr = bar_new()
    }

    func use() {
        bar_use(ptr)
    }

    deinit {
        bar_free(ptr)
    }
}


@_expose(wasm, "test2")
@_cdecl("test2")
func test2() {
    let bar = Bar()
    bar.use()
}

gets compiled to:

export function test2() {
  var a:int = bar_new();
  bar_free(a);
  bar_use(a);
}

Note the order: Why is bar_free called before bar_use?

Is this expected? If so, how so? If not, are these bugs? They might not be related to WebAssembly or Embedded at all, it is just the combination I'm trying out and can easily reproduce locally.

cc @Max_Desiatov @kateinoigakukun @kubamracek @Andrew_Trick

2 Likes

I can reproduce this. Looks like a compiler bug to me. Would you mind opening a GitHub issue for it?

1 Like

Thank you for your quick reply! I opened Bug in Embedded/ARC/WebAssembly combination? · Issue #73768 · apple/swift · GitHub

1 Like

This should be fixed with Don't delete allocations in DCE by meg-gupta · Pull Request #73852 · apple/swift · GitHub -- so whenever a new toolchain comes out on Swift.org - Download Swift, could you try it to see if this gets fixed? Thanks!

3 Likes

Awesome, thank you and especially @meg-gupta :clap:

2 Likes

There was another bug which caused miscompilation, but that one is also fixed now: Bug in Embedded/ARC/WebAssembly combination? · Issue #73768 · apple/swift · GitHub :tada:

1 Like