emscripten-core / emscripten-core/emscripten

Why doesn't `RegisteredPointer` dereference object via its `$$.smartPtr` if one is defined?

Open
#17,765 2 comments 0 reactions 0 assignees View on GitHub
embind
Dominant language
C++
Stars
27.6k
Forks
3.6k
Avg merge
1d 1h
Merged PRs (30d)
105

Description

In order to improve object lifetime management semantics, I'm attempting to return/emit objects from my compiled webassembly module via smart pointers. However, using `std::shared_ptr` is not ideal since it needs to be explicitly deleted and has unclear lifetime; I don't want to let JS variables increment reference count of my objects.

Instead, I specialized `emscripten::smart_ptr_trait` for the `std::weak_ptr` type, and put logic into its `get()` function to throw an exception if, when attempting to retrieve the raw pointer, the `weak_ptr` instance discovers the object is expired. Here's an example implmentation:
```C++
namespace emscripten {

template
struct smart_ptr_trait> {
typedef std::weak_ptr PointerType;
typedef typename PointerType::element_type element_type;

static element_type* get(const PointerType& ptr) {
std::cout << "TRYING to lock weak pointer!" << std::endl;
auto shared_ptr = ptr.lock();
if (!shared_ptr) {
std::cout << "weak pointer was expired!" << std::endl;
throw std::runtime_error("Disposed object was dereferenced!");
}
std::cout << "successfully got pointer to object!" << std::endl;
return shared_ptr.get();
}

static sharing_policy get_sharing_policy() {
return sharing_policy::BY_EMVAL;
}

static std::weak_ptr* share(PointeeType* p, EM_VAL v) {
std::shared_ptr shared_ptr(
p,
val_deleter(val::take_ownership(v)));
return new std::weak_ptr(shared_ptr);
}

static PointerType* construct_null() {
return new PointerType;
}

private:
class val_deleter {
public:
val_deleter() = delete;
explicit val_deleter(val v)
: v(v)
{}
void operator()(void const*) {
v();
// eventually we'll need to support emptied out val
v = val::undefined();
}
private:
val v;
};
};

} // namespace emscripten
```

This works when first returning an object from webassembly to the JS layer. That is, if a C++ `std::weak_ptr` is returned from a function bound with embind, when initially creating the `RegisteredPointer` object an exception will be thrown if that `weak_ptr` is already expired. Example below (note the dtors set `valid` to `false`)

C++ object bindings:
```C++
class TestBase {
public:
bool valid { true };
TestBase() : valid(true) {};
~TestBase() { valid = false; };
virtual string foo() {
return "foo";
}
virtual bool baseValid() { return valid; };
};

static std::shared_ptr instance = nullptr; // toy "object manager" for a singleton

static void MakeTestBase() {
instance = std::make_shared();
}

static std::weak_ptr GetTestInstance() {
return instance; // constructs a weak_ptr from the shared_ptr, and embind glue throws if `instance` is null.
}

static void RemoveTestInstance() {
instance = nullptr; // deletes the only handle to object, which destroys it
}

EMSCRIPTEN_BINDINGS(MyModule){
class_("TestBase")
.smart_ptr>("TestBase")
.function("baseValid", &TestBase::baseValid)
.function("foo", &TestBase::foo);

function("MakeTestBase", &MakeTestBase);
function("GetTestInstance", &GetTestInstance);
function("RemoveTestInstance", &RemoveTestInstance);
}
```
Example javascript using the compiled wasm/js:
```javascript
let module = MyModule();
try {
let obj = module.GetTestInstance();
} catch (e) {
console.log("This throws as expected");
}
module.MakeTestBase();
let obj = module.GetTestInstance(); // works!
console.log(obj.baseValid()); // true
console.log(obj.foo()); // prints "foo"
```

Hooray!

However, if in the JS layer I retrieve the object while valid, then perform some operation that ensures the object is disposed, the `RegisteredPointer` instance goes ahead and dereferences the object, ostensibly using its `$$.ptr` value (the raw pointer retrieved from the initial `get()` when first creating the `RegisteredPointer` instance). Using the same bindings:

```javascript
let module = MyModule();
module.MakeTestBase();
let obj = module.GetTestInstance();
console.log(obj.baseValid()); // true
module.RemoveTestInstance();
console.log(obj.baseValid()); // false; dtor has been called
console.log(obj.foo()); // still prints "foo"
```

My intuition would be that if a `smart_ptr` type is registered, and a `RegisteredPointer` has one, that it is used to retrieve the object _each time it is dereferenced_. If that were the case, we would presumably use `smart_ptr_trait>::get()` to get the pointer which would throw an error on expired objects, as desired.

My question is: Why isn't this the case? What are the technical barriers preventing this from happening, and are there any workarounds? (Or, if this is user error, how do I get this working?)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.