diff --git a/README.md b/README.md index a4a2507..e6b8e19 100644 --- a/README.md +++ b/README.md @@ -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 getResource() { + static std::shared_ptr resource = std::make_shared(); + 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). diff --git a/include/dukglue/detail_primitive_types.h b/include/dukglue/detail_primitive_types.h index 77a82ce..3253081 100644 --- a/include/dukglue/detail_primitive_types.h +++ b/include/dukglue/detail_primitive_types.h @@ -1,10 +1,12 @@ #pragma once #include "detail_types.h" +#include "detail_typeinfo.h" #include "dukvalue.h" #include #include +#include // for std::shared_ptr namespace dukglue { namespace types { @@ -155,6 +157,72 @@ namespace dukglue { } }; + // std::shared_ptr (as value) + template + struct DukType< std::shared_ptr > { + typedef std::true_type IsValueType; + + static_assert(std::is_same::IsValueType, std::false_type>::value, "Dukglue can only use std::shared_ptr to non-value types!"); + + template + static std::shared_ptr 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(duk_get_pointer(ctx, -1)); + if (!info->can_cast()) + 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*) ptr); + } + + static duk_ret_t shared_ptr_finalizer(duk_context* ctx) + { + duk_get_prop_string(ctx, 0, "\xFF" "shared_ptr"); + std::shared_ptr* ptr = (std::shared_ptr*) 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 + static void push(duk_context* ctx, const std::shared_ptr& value) { + dukglue::detail::ProtoManager::make_script_object(ctx, value.get()); + + // create + set shared_ptr + duk_push_pointer(ctx, new std::shared_ptr(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 struct DukType< std::function > { diff --git a/tests/test_primitives.cpp b/tests/test_primitives.cpp index 279682a..e9059f4 100644 --- a/tests/test_primitives.cpp +++ b/tests/test_primitives.cpp @@ -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(); @@ -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("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 >(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);