Skip to content

Embind regression: Leaked C++ object when calling from C++ to JS #22575

Open
@stbergmann

Description

@stbergmann

I originally mentioned this in a comment at #21022 (comment) "embind: Don't automatically destroy arguments passed into val calls. (#21022)", but it likely got missed there:

Since 6e54690 "embind: Don't automatically destroy arguments passed into val calls. (#21022)" was merged into 3.1.52, the below test program behaves differently than before, and I wonder whether that's intentional. The test setup is

$ cat test.cc
#include <iostream>
#include <memory>

#include "emscripten.h"
#include "emscripten/bind.h"

class Smart {
public:
  Smart(): id_(counter++) { std::cout << "create " << id_ << "\n"; }
  Smart(Smart const & other): id_(counter++) {
    std::cout << "copy " << other.id() << " to " << id_ << "\n";
  }
  ~Smart() { std::cout << "destroy " << id_ << "\n"; }
  int id() const { return id_; }
private:
  void operator =(Smart) = delete;
  static unsigned counter;
  int const id_;
};

unsigned Smart::counter = 0;

std::shared_ptr<Smart> getSmart() {
  return std::make_shared<Smart>();
}

struct Interface {
  virtual void memFun(Smart const &) const {};
};

struct Wrapper: emscripten::wrapper<Interface> {
public:
  EMSCRIPTEN_WRAPPER(Wrapper);
  void memFun(Smart const & smart) const override {
      call<void>("memFun", smart);
  }
};

void useInterface(Interface const & ifc) {
  std::shared_ptr<Smart> s(getSmart());
  ifc.memFun(*s);
}

EM_JS(void, jsRun, (), {
  const Impl = Module.Interface.extend('Interface', {
    memFun(smart) { console.log('use ' + smart.id()); }
  });
  const impl = new Impl();
  const s = Module.getSmart();
  impl.memFun(s);
  impl.memFun(s);
  s.delete();
  Module.useInterface(impl);
});

EMSCRIPTEN_BINDINGS(test) {
  emscripten::class_<Smart>("Smart")
    .smart_ptr<std::shared_ptr<Smart>>("SmartPtr")
    .function("id", &Smart::id);
  emscripten::class_<Interface>("Interface")
    .function("memFun", &Interface::memFun)
    .allow_subclass<Wrapper>("Wrapper");
  emscripten::function("getSmart", &getSmart);
  emscripten::function("useInterface", &useInterface);
}

int main() {
  jsRun();
}

and

$ em++ test.cc -lembind -o test.html && emrun test.html

Until 3.1.51 (also in 3.1.47 and 3.1.48, i.e., irrespective of 12777ca "[emval] Reuse method calling optimisation in regular calls (#20383)" that was merged into 3.1.48 and motivated this change), the browser console logs were

create 0
use 0
use 0
destroy 0
create 1
copy 1 to 2
use 2
destroy 2
destroy 1

That is., the instance of Smart used in the call to JS ifc.memFun(*s); from C++ useInterface is properly destroyed.

Since 3.1.52, the browser console logs instead are

create 0
use 0
use 0
destroy 0
create 1
copy 1 to 2
use 2
destroy 1

That is, the instance of Smart used in the call to JS ifc.memFun(*s); from C++ useInterface is leaked now. But even if the new behavior is intentional: Where should that instance be destroyed? I think this commit assumes that the JS implementation

    memFun(smart) { console.log('use ' + smart.id()); }

should smart.delete(); it now. But then, that would apparently break the sequence of direct JS to JS calls

  impl.memFun(s);
  impl.memFun(s);

from within jsRun.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions