Skip to content

Commit

Permalink
Add support for std::shared_ptr<T>, with caveats.
Browse files Browse the repository at this point in the history
See README.md for details - tl;dr dynamic properties won't persist (since
Dukglue doesn't keep an internal reference to them in the heap stash) and
equality checks won't work (since they are just two separate
std::shared_ptr objects that happen to be pointing to the same thing).
  • Loading branch information
Aloshi committed Feb 9, 2017
1 parent a47362e commit 217d6ca
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 0 deletions.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,37 @@ What Dukglue **doesn't do:**

Basically, for this to be possible, Duktape needs support for weak references, so Dukglue can keep references without keeping them from being garbage collected.

* Dukglue supports `std::shared_ptr`, but with two major caveats:

1. **Dynamic properties will not persist** - any properties not defined with `dukglue_register_property` will not be the same between two shared_ptrs pointing to the same object. For example:

```cpp
std::shared_ptr<Resource> getResource() {
static std::shared_ptr<Resource> resource = std::make_shared<Resource>();
return resource;
}
```

```js
var resource = getResource();
tex.isAwesome = true;
var resourceAgain = getResource();
print(resourceAgain.isAwesome); // prints undefined
print(tex.isAwesome); // prints true
```

(However, dynamic properties will stay on the object once it has entered script until it is garbage collected - `tex.isAwesome` will still be `true`.)

One minor upside to properties not persisting is that **std::shared_ptr objects don't need to call `dukglue_invalidate_reference()` when they are destroyed**.

2. **JavaScript equality checks will not work**. This is because the current implementation treats shared_ptrs like a value type.

```js
print(tex === texAgain); // prints false
```

I'm not really happy with these caveats, but it was the fastest way to implement and it is acceptable for my use case. The entire implementation is in detail_primitive_types.h if you want to try and improve it (maybe try using Duktape's ES6 proxy subset?).

* Dukglue *might* not follow the "compact footprint" goal of Duktape. I picked Duktape for it's simple API, not to script my toaster. YMMV if you're trying to compile this for a microcontroller. Why?

* Dukglue currently needs RTTI turned on. When Dukglue checks if an object can be cast to a particular type, it uses the typeid operator to compare if two types are equal. It's always used on compile-time types though, so you could implement it without RTTI if you needed to. Dukglue also uses exceptions in two places: the `dukglue_pcall*` functions (since these return a value instead of an error code, unlike Duktape), and the `DukValue` class (to communicate type errors on getters and unsupported types).
Expand Down
68 changes: 68 additions & 0 deletions include/dukglue/detail_primitive_types.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#pragma once

#include "detail_types.h"
#include "detail_typeinfo.h"
#include "dukvalue.h"

#include <vector>
#include <stdint.h>
#include <memory> // for std::shared_ptr

namespace dukglue {
namespace types {
Expand Down Expand Up @@ -155,6 +157,72 @@ namespace dukglue {
}
};

// std::shared_ptr (as value)
template<typename T>
struct DukType< std::shared_ptr<T> > {
typedef std::true_type IsValueType;

static_assert(std::is_same<typename DukType<T>::IsValueType, std::false_type>::value, "Dukglue can only use std::shared_ptr to non-value types!");

template <typename FullT>
static std::shared_ptr<T> read(duk_context* ctx, duk_idx_t arg_idx) {
if (duk_is_null(ctx, arg_idx))
return nullptr;

if (!duk_is_object(ctx, arg_idx))
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected shared_ptr object", arg_idx);

duk_get_prop_string(ctx, arg_idx, "\xFF" "type_info");
if (!duk_is_pointer(ctx, -1)) // missing type_info, must not be a native object
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: expected shared_ptr object (missing type_info)", arg_idx);

// make sure this object can be safely returned as a T*
dukglue::detail::TypeInfo* info = static_cast<dukglue::detail::TypeInfo*>(duk_get_pointer(ctx, -1));
if (!info->can_cast<T>())
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: wrong type of shared_ptr object", arg_idx);
duk_pop(ctx); // pop type_info

duk_get_prop_string(ctx, arg_idx, "\xFF" "shared_ptr");
if (!duk_is_pointer(ctx, -1))
duk_error(ctx, DUK_RET_TYPE_ERROR, "Argument %d: not a shared_ptr object (missing shared_ptr)", arg_idx);
void* ptr = duk_get_pointer(ctx, -1);
duk_pop(ctx); // pop pointer to shared_ptr

return *((std::shared_ptr<T>*) ptr);
}

static duk_ret_t shared_ptr_finalizer(duk_context* ctx)
{
duk_get_prop_string(ctx, 0, "\xFF" "shared_ptr");
std::shared_ptr<T>* ptr = (std::shared_ptr<T>*) duk_require_pointer(ctx, -1);
duk_pop(ctx); // pop shared_ptr ptr

if (ptr != NULL) {
delete ptr;

// for safety, set the pointer to undefined
// (finalizers can run multiple times)
duk_push_undefined(ctx);
duk_put_prop_string(ctx, 0, "\xFF" "shared_ptr");
}

return 0;
}

template <typename FullT>
static void push(duk_context* ctx, const std::shared_ptr<T>& value) {
dukglue::detail::ProtoManager::make_script_object(ctx, value.get());

// create + set shared_ptr
duk_push_pointer(ctx, new std::shared_ptr<T>(value));
duk_put_prop_string(ctx, -2, "\xFF" "shared_ptr");

// set shared_ptr finalizer
duk_push_c_function(ctx, &shared_ptr_finalizer, 1);
duk_set_finalizer(ctx, -2);
}
};

// std::function
/*template <typename RetT, typename... ArgTs>
struct DukType< std::function<RetT(ArgTs...)> > {
Expand Down
62 changes: 62 additions & 0 deletions tests/test_primitives.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,30 @@ std::string* get_ptr_cpp_string() {
return &str;
}

class Dog {
public:
Dog(const char* name) : mName(name) {
sCount++;
}
virtual ~Dog() {
sCount--;
}

static int count() {
return sCount;
}

inline const std::string& name() const {
return mName;
}

private:
static int sCount;
std::string mName;
};

int Dog::sCount = 0;

void test_primitives() {
duk_context* ctx = duk_create_heap_default();

Expand Down Expand Up @@ -106,6 +130,44 @@ void test_primitives() {
test_assert(nums.at(2) == 3);
}

// std::shared_ptr
{
test_assert(Dog::count() == 0);

auto dog = std::make_shared<Dog>("Archie");
test_assert(Dog::count() == 1);

// can we push it?
dukglue_push(ctx, dog);
test_assert(Dog::count() == 1);

// save it somewhere - does the shared_ptr persist? (i.e. deleter not called)
duk_put_global_string(ctx, "testDog");
test_assert(Dog::count() == 1);

dog.reset();
test_assert(Dog::count() == 1);

// can we read it?
duk_get_global_string(ctx, "testDog");
dukglue_read< std::shared_ptr<Dog> >(ctx, -1, &dog);
duk_pop(ctx);

test_assert(dog->name() == "Archie");
test_assert(Dog::count() == 1);
dog.reset();

// remove it completely (should trigger shared_ptr deleter after GC)
duk_push_undefined(ctx);
duk_put_global_string(ctx, "testDog");

// intentionally called twice to make sure objects with finalizers are collected (see duk_gc docs)
duk_gc(ctx, 0);
duk_gc(ctx, 0);

test_assert(Dog::count() == 0);
}

test_assert(duk_get_top(ctx) == 0);
duk_destroy_heap(ctx);

Expand Down

0 comments on commit 217d6ca

Please sign in to comment.