pmed / v8pp

Bind C++ functions and classes into V8 JavaScript engine
http://pmed.github.io/v8pp/
Other
901 stars 120 forks source link

Question: mapping nested object's properties and JS classes. #17

Closed nouknouk closed 9 years ago

nouknouk commented 9 years ago

Hi,

I would have a few questions about properties (JsWrapped) objects nested in other JSWrapped objects. Let's say I have a Sprite c++ class embedding a Coord:

class Coord {
public:
    Coord() : x(0), y(0) {}
    int x,y;
};
class Sprite {
public:
      Sprite() : position() {}
      Coord position;
};

(step 1) I wrap them using the class_ helpers:

v8pp::class_<Coord> JS_Coord(isolate);
JS_Coord.ctor<>();
JS_Coord.set("x", v8pp::property(&Coord::x));
JS_Coord.set("y", v8pp::property(&Coord::y));

v8pp::class_<Sprite> JS_Sprite(isolate);
JS_Sprite.ctor<>();
JS_Sprite.set("position", v8pp::property(&Sprite::position));

(step 2) in my JS script, I instanciate a new Sprite.

s = new Sprite();
console.log("pos type is "+typeof(s.position));

Getting the position returns 'undefined'.

I understood it was because the Coord instance was not known from class_<Coord>::find_object(), because it has not been instanciated by the v8pp wrappers, rather by my Sprite constructor's class, so on "C++ side"

(step 3) I can manage such problem by changing the find_object() function, adding kind of 'auto external's referencement':

static v8::Handle<v8::Object> find_object(v8::Isolate* isolate, T const* obj) {
    // as before
    v8::Handle<v8::Object> jsObj = class_singleton::instance(isolate).find_object(obj);
    // fallback added if not already registered in singleton:
    if (jsObj.IsEmpty()) {
        jsObj = v8pp::class_<T>::reference_external(isolate, ((T*)obj) );
    }
    return jsObj;
}

But now, the problem is that when I delete my initial object (the Sprite), (i suppose) the Coord instance will remain indefinitely registered in the singleton<Coord> (with a dangling pointer).

=> (I'm new to v8pp, so it's probable that) I missed something ?

=> To manage the mapping of the object's lifetimes between "C++ side" and "JS side", should i foresee something the like usage of 'smart pointers' with kind of callback to v8pp when an object is deleted, and also using "smart_ptr aliasing" to recursively delete nested objects (Sprite deleted => delete Coord) ?

Any idea would be highly welcome. Thanks in advance.

pmed commented 9 years ago

Hi,

C++ class wrapping with v8pp::class_ is supposed to use for object construction in JavaScript side with new. And the v8pp::class_<T>::reference_external() function is rather to make visible in JavaScript an existing C++ object, say some global one.

For simple C++ types like structs I use a way with custom type converter - to specialize v8pp::convert for the type. The v8pp::convert<T> static functions is used in v8pp::to_v8() and v8pp::from_v8() to convert a C++ type into a corresponding V8 type and vice versa.

For your Coord it will be like this:


struct Coord
{
    int x, y;
};

namespace v8pp {

template<>
struct convert<Coord>
{
    using from_type = Coord;
    using to_type = v8::Handle<v8::Object>;

    static bool is_valid(v8::Isolate*, v8::Handle<v8::Value> value)
    {
        return value->IsObject();
    }

    static from_type from_v8(v8::Isolate* isolate, v8::Handle<v8::Value> value)
    {
        v8::HandleScope scope(isolate);

        if (value->IsObject())
        {
            v8::Local<v8::Object> obj = value->ToObject();
            Coord result;

            if (get_option(isolate, obj, "x", result.x) && get_option(isolate, obj, "y", result.y))
            {
                return result;
            }
        }
        throw std::invalid_argument("expected {x, y} object");
    }

    static v8::Handle<v8::Value> to_v8(v8::Isolate* isolate, Coord const& value)
    {
        v8::EscapableHandleScope scope(isolate);

        v8::Local<v8::Object> obj = v8::Object::New(isolate);
        set_option(isolate, obj, "x", value.x);
        set_option(isolate, obj, "y", value.y);

        return scope.Escape(obj);
    }
};

} // namespace v8pp
nouknouk commented 9 years ago

Hi, and thanks for your reply.

I agree for simple structs. My example was probably too trivial, but I would have other -more complex- types which would be sometimes created on C++ side, sometimes on JS side.

Another example: a (tree organized) Sprite class, with several specialized subclasses (Text, Image, Polygon, ...), with their own properties (text, imageTexture, polygonPoints, other complex objects, ...). Those Sprite instance can contain themselves a list of other sprites (its 'children'), to compose more complex graphical entities, aka "widgets" (let's say a "Button" widget, composed of an Image + a Text + ...).

Especially because of the second point, the Sprite instances must be instanciated from both C++ & JS side.

That's why I was thinking about the usage of smart_ptr, to address the problem of shared ownership between JS & C++ sides: letting my C++ framework use shared_ptr, and adding shared_ptr usage in v8pp. This would avoid an early deletion of an instance whereas the 'other side' still references it, and not forgetting to delete pending unused instances when nobody references them anymore.

What do you think about the basic concept ?

pmed commented 9 years ago

For more complex class hierarchies you may try v8pp::persistent_ptr<T> template. I used it to store a pointer to C++ object and V8 persistent handle. But C++ object should be wrapped with v8pp::class_ and exposed to JavaScript with v8pp::class_<T>::import_external()

Something like this:

struct Coord
{
    int x, y;
};

class Sprite
{
    v8pp::persistent_ptr<Coord> pos_;
public:
   Coord& position() { return *pos_; }
};

v8pp::class_<Coord> Coord_JS(isolate);
Coord_JS.set("x", &Coord::x).set("y", &Coord::y);

v8pp::class_<Sprite> Sprite_JS(isolate);
Sprite_JS.set("position", v8pp::property(&Sprite::position));

Sprite::Sprite()
{
    // create C++ object
     Coord* pos = new Coord;
    // expose it to JS
     v8::Local<v8::Value> js_pos = v8pp::class_<Coord>::import_external(isolate, pos);
    // make JS handle persistent
     pos_.reset(isolate, js_pos);
}

Probably class_::reference_external should work too for a pointer to aggregated object. Like this:

struct Coord
{
    int x, y;
};

class Sprite
{
    Coord pos_;
public:
   Coord& position() { return pos_; }
};

v8pp::class_<Coord> Coord_JS(isolate);
Coord_JS.set("x", &Coord::x).set("y", &Coord::y);

v8pp::class_<Sprite> Sprite_JS(isolate);
Sprite_JS.set("position", v8pp::property(&Sprite::position));

Sprite::Sprite()
{
    // just reference to pos_ object, it will alive while this Sprite is alive
    // expose pos_ to JS
     v8pp::class_<Coord>::reference_external(isolate, &pos_);
}

In this case there should be opposite to reference_external() function, say v8pp::class_<T>::unreference(v8::Isolate*, T* object> to remove the object from internal list of persistent handles in v8pp::class_:

Sprite::~Sprite()
{
     v8pp::class_<Coord>::unreference(isolate, &pos_);
}
nouknouk commented 9 years ago

Thanks a lot for your help, I will dig into that.

tjgerrylee commented 7 years ago

Hi nouknouk:

i think you can just modify the find_object function. the dangling pointer problem maybe is because the issue in reference_external, it add an external pointer in class_info's object list then delete it, the map.emplace fuction just copy the pointer as first of std::pair.