pmed / v8pp

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

problem with pointers #69

Closed MirrorGuard closed 6 years ago

MirrorGuard commented 6 years ago

In the most cases pointers are converted to an array/vector/string (e.t.c), but sometimes that is wrong behavior and I have found no way to workaround it. It is so, when pointers are used as an optional references (when null-value is a possible; as something like references in a Java or a C#, for example).

Following pseudocode snippet shows what am I talking about:

class WithOptionalData{
public:
//Returns pointer to optional data or nullptr (not an array)
const T* getOptionalData() const;

/*Other methods and data*/
};

For class WithOptionalData, const T* means, that it mostly returns data, but "null-value" is possible. Using std::optional instead of T* sometimes impossible or inefficient:

In fact, with class WithOptionalData, while using:

v8pp::class_<WithOptionalData> c{isolate};

c.set("getOptionalData", &WithOptionalData::getOptionalData);
//or alternative version, used mutually exclusive with one above
c.set("OptionalData", v8pp::property(&WithOptionalData::getOptionalData));

compile time error occur with v8pp::to_v8, deep inside in v8pp library:

Maybe I am doing wrong something, but there is no documented way to deal with methods, that returns pointers to requested values (when pointer means "optional", not an array).


Other (second) problem with pointers looks like this:

class Transform{//Some class deals with 2 (or 3 - doesent matter) dimentional transformations
float matrix_[16];
public:
/*Some methods*/

//And this error prone one
const float* getMatrix() const;
};

While binding, using v8pp::class_:

v8pp::class_<Transform> c{isolate};

c.set("getMatrix", &Transform::getMatrix);
//or alternative version, used mutually exclusive with one above
c.set("Matrix", v8pp::property(&Transform::getMatrix));

It looks like that code above should work, but it doesnt. Despite that getMatrix returns an array and it is a correct behavior (and v8pp can deal with arrays), something goes wrong and it looks like that const float* processed as a special kind of a string (where character represented as a float-value). Evidently, that it is a constant pointer to the internal array, not a some kind of string. But deeply inside the library, const float * is processed as a string, not an array of float, that is wrong and leads to compile time error deep inside v8pp, somewhere with to_v8.

Although there is a way to workaround this (second problem), using static functions/lamdas, like

.set("getMatrix",

 [](v8::FunctionCallbackInfo<v8::Value> const& args){
Transform* self = class_<Transform>::unwrap_object(args.GetIsolate(), args.This());
if (self) {

//gsl - guideline support library - https://github.com/Microsoft/GSL
//and https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md
gsl::span<const float> matrix(self->getMatrix(), 9);
args.GetReturnValue().Set(v8pp::to_v8(args.GetIsolate(), matrix.begin(), matrix.end()));

}else args.GetReturnValue().Set(args[0]);
});

but it is unusual and inconvenient way to do such things.


Anyway, is there a way to deal with pointers using v8pp, when: 1)Pointer points to an optional data (when data or nullptr is returned) 2)Pointer is a const primitive pointer to an array and it looks like that it is processed as a some kind of string (instead of a correct behavior, when it is processed as an array/vector, like in example with a const float*) ?


Thank you in advance for your answer.

pmed commented 6 years ago

The library has no conversion support for primitive pointer types (e.g. int* or float*) intentionally.

Sure, one would add such a converter:

// helper meta function
template<typename T>
using is_character_type = std::integral_constant<bool,
    std::is_same<T, char>::value ||
    std::is_same<T, char16_t>::value ||
    std::is_same<T, wchar_t>::value>;

// converter for non-const T* that is not wrapped and not character type
template<typename T>
struct convert<T*, typename std::enable_if<
    !is_wrapped_class<T>::value &&
    !is_character_type<T>::value &&
    !std::is_const<T>::value>::type>
{
    using from_type = T*;
    //using to_type = decltype(convert<T>::to_v8(std::declval<v8::Isolate*>(), std::declval<T>()));
    using to_type = v8::Local<v8::Value>;

    static bool is_valid(v8::Isolate*, v8::Local<v8::Value> value)
    {
        return true;
    }

    static from_type from_v8(v8::Isolate* isolate, v8::Local<v8::Value> value)
    {
        if (value.IsEmpty() || value->IsNullOrUndefined())
        {
            return nullptr;
        }
        return &(convert<T>::from_v8(isolate, value)); // (1)
    }

    static to_type to_v8(v8::Isolate* isolate, T* value)
    {
        if (!value) return v8::Null(isolate);  // (2)
        else return convert<T>::to_v8(isolate, *value);
    }
};

But there are 2 issues in this converter:

  1. How to return a pointer from_v8() function, because at the line (1) it returns an address of temporary value (might be related to #39)
  2. What return type has to be for to_v8() function, because at the line (2) it is v8::Local<v8::Primitive> and we have to use least common denominator v8::Local<v8::Value>

In my opinion, usage of std::optional<T> for optional return values, and std::array for arrays looks better.

MirrorGuard commented 6 years ago

Sorry for my late response if it is too late.

I completely agree with you, that retuning std::optional and std::array are much better than raw pointers in the most cases. But unfourtenatly my question related to the least part of cases, such as external APIs, third party libraries that are at least difficult or at most impossible to rewrite.

I didnt talk about exactly providing a way to deal with raw pointers inside javascrpt. But I'm looking for a way to provide optionality and or access to internal data, that is returned optionally. And as I talked alredy, I assume an external API, not my own code, that I would like to bind, something not opensource and/or something with a large code base.

Just to do in javascript something like (in a context of optionality: "null or a value") this:

var v=fuctionFromCpp();
if(v==null)
/*do something if no any value*/
else{
/*working with value here*/
}

whereas in C++ I have an external API like this:

//That is documented, that this function (or a method) returns a NULL or a pointer to the valid value
//and this is not required to delete/free this manually. 
//And there is a strict guarantee that the *target* the pointer points to is existing enough time 
//to work with it (including in javascript too). 
//But the problem is: how to represent it's optionality?
T* functionInCpp();//Or a class method.

I know, according to v8pp documentation, that there is a way to wrap std::vector and std::array, but im not sure about std::optional. Moreover, my request about pointers covertion (where "pointer" is a representation of an optional value and does the same job as an exact std::optional) is very similar to convertion of a an exact std::optional.

So, may be it would be better to ask, "if there in v8pp a convertion of an std::optional?" And if so, why there is no convertion of the raw pointer T* the same way as std::optional?

About the lines (1) and (2) you wrote: What about v8::Object and it's internal fields (I mean the Get/Set AlignedPointerFromInternalField)?

(1)

return (value->IsObject())?
static_cast<from_type>(Object::Cast(value)->GetAlignedPointerFromInternalField(INDEX)):
nullptr;

I'm not sure about exact index, but in my opinion, conceptually this is the right direction to solve the optionality problem.

(2) Again this looks like that it should be an v8::Object.

For the most cases that is assumed, that T is the pointer to an object of T the type (struct or a class). So, there is no much difference between the wrapping that v8pp does and has to be done, except the setting and getting internal pointer (exact origial* pointer C++ to the C++ object).

Unfortunately for primitives (such as int*, for example) the tiny workaround required:

//For simplicity this is not a template and I assume that T=int.
//Also I've discard specific almost all int operations (except one) and a wrapping code,
//where class RefWrapperInt wrapped with v8pp::class_<RefWrapperInt> - just for short.

class Ref_wrapper_int{
public: 
Ref_wrapper_int(int * v):v_(v){}

//operation example
void add(int a){
if(v_)*v_+=a;
}

//this is for implicit convertion to int*
operator int*(){
return v_;
}

private:
int * v_;
}

And so, if T == int, at the (1) line from_v8 is a Ref_wrapper_int from_v8(Isolate*, Handle<Value>)

and at the (2) line to_v8 is a v8::Handle<Object> to_v8(Isolate*, int*)

Remark: For primitives there is no InternalFields (AlignedPointerFromInternalField) required, so my first example: return (value->IsObject())? static_cast<from_type>(Object::Cast(value)->GetAlignedPointerFromInternalField(INDEX)): nullptr; wich is not adapted for primitives, is incorrect in case of primitives, wrapped using workaround similar to the Ref_wrapper_int from my example. But this are details.

Is there all clear in my reasoning, or I've missed somthing important (or have a mistake)?

pmed commented 6 years ago

Well, this Ref_wrapper_int seems much like std::optional<int> for me. And it seems that you will have to writer wrapper functions/lambdas to deal with pointers to optional objects from a 3rd party library. Sorry, I can't see a magic universal conversion for that.

MirrorGuard commented 6 years ago

Ок. This is not exactly what I would like to know, but it is a fact, so I have nothing to do than to close this issue, because of impossibility to solve it in a convenient way. You spent a time during answer for my questions, so thank you not for a solution, but for your answers.