emscripten icon indicating copy to clipboard operation
emscripten copied to clipboard

How to create value_object binding with optional fields?

Open mean-ui-thread opened this issue 7 years ago • 7 comments

consider the following example:

struct Rectangle {
   int x = 0;
   int y = 0;
   int width = 100;
   int height = 100;
   int color = 0xFF000000;
};

class Shape {
public: 
    Shape(Rectangle rect);  
};

EMSCRIPTEN_BINDINGS(shapes) {

value_object<Rectangle>("Rectangle")
    .field("x", &Rectangle::x)
    .field("y", &Rectangle::y)
    .field("width", &Rectangle::width)
    .field("height", &Rectangle::height)
    .field("color", &Rectangle::color);

class_<Shape>("Shape")
    .constructor<Rectangle>();

}

Then works flawlessly when I do this in Javascript:

var rect = new Module.Shape( {x: 50, y: 60, width:250, height:10, color: 0xFF000000} )

However, if I just want to only provide the fields I want to override and leave the other fields to their default value like so:

var rect = new Module.Shape( {x: 50, y: 60, width:250, height:10} )

then I get the following error:

Uncaught TypeError: Missing field

How do we make optional fields with embind's value_object? Thanks!

mean-ui-thread avatar Feb 11 '18 16:02 mean-ui-thread

I think I figured out a quick work around: I manually modified emsdk-portable/emscripten/1.37.33/src/embind/embind.js from:

            'toWireType': function(destructors, o) {
                // todo: Here we have an opportunity for -O3 level "unsafe" optimizations:
                // assume all fields are present without checking.
                for (var fieldName in fields) {
                    if (!(fieldName in o)) {
                        throw new TypeError('Missing field');
                    }
                }
                var ptr = rawConstructor();
                for (fieldName in fields) {
                    fields[fieldName].write(ptr, o[fieldName]);
                }
                if (destructors !== null) {
                    destructors.push(rawDestructor, ptr);
                }
                return ptr;
            },

to

            'toWireType': function(destructors, o) {
                var ptr = rawConstructor();
                for (fieldName in fields) {
                    if (fieldName in o) {
                        fields[fieldName].write(ptr, o[fieldName]);
                    }
                }
                if (destructors !== null) {
                    destructors.push(rawDestructor, ptr);
                }
                return ptr;
            },

and I think it works now.

mean-ui-thread avatar Feb 11 '18 17:02 mean-ui-thread

You can use emscripten::val && hasOwnProperty(width)

===

You can use emscripten::val && hasOwnProperty to create overloaded constructors.

We can call native constructor based on properties && typeof:

    .constructor<emscripten::val>( select_overload<ClassNameHere(emscripten::val)>([](emscripten::val c) {
       std::cout << "int constructor wrapped with emscripten::val" << std::endl;
       if (c.typeof().as<std::string>() == "string") {
           return ClassNameHere("1");
       } else if (c.typeof().as<std::string>() == "boolean") {
           return ClassNameHere(true);
       }
       return ClassNameHere(1);
    }))

typeof(emscripten::val) === string -> call string contructor typeof(emscripten::val) === boolean -> call bool contructor e.t.c.


template<typename T> 
class TemplatedConstructor {
private:
    T value;
    int p1;
    int p2;
    std::string str;

public:
    TemplatedConstructor() {};
    TemplatedConstructor(T val) : value(val) {};
    TemplatedConstructor(std::string val) : str(val) {
        //std::cout << "TemplatedConstructor(std::string val) called" << std::endl;
    };
    TemplatedConstructor(int a1, int a2) { p1 = a1; p2 = a2; };

    void set(T val) {
        value = val;
        str = std::to_string(value);
    }

    T get() {
        std::cout << "str:" << str << std::endl;
        std::cout << "p1:" << p1 << std::endl;
        std::cout << "p2:" << p2 << std::endl;
        return value;
    }
};

//......

EMSCRIPTEN_BINDINGS(my_module) {
class_<TemplatedConstructor<int>>("TemplatedConstructor")
    .constructor<>()
    .constructor<int>()
 // cant use
    //.constructor<std::string>()
 // cant use
    //.constructor<std::string>(select_overload<void(std::string)>(&TemplatedConstructor<std::string>::TemplatedConstructor<std::string>))
    .constructor<int, int>()
 // change it to accept one argument
    // Idea: use emscripten::val && hasOwnProperty to create overloaded constructors. We can call native constructor based on properties
    .constructor<std::string>( select_overload<TemplatedConstructor<int>(std::string, std::string, emscripten::val)>([](std::string a, std::string b, emscripten::val c) {
       std::cout << "wrap constructor in lambda 1" << std::endl;
       std::cout << "c.typeof().as<std::string>() is " << c.typeof().as<std::string>() << std::endl;
       if (c.hasOwnProperty("isStr")){
           std::cout << "hasOwnProperty isStr" << std::endl;
           std::cout << "c[\"isStr\"].typeof().as<std::string>() is " << c["isStr"].typeof().as<std::string>() << std::endl;
           std::cout << "c[\"value\"].typeof().as<std::string>() is " << c["value"].typeof().as<std::string>() << std::endl;
           //emscripten::val strVal = c["isStr"];
           //std::string getVal;
           std::cout << "strVal: " << c["value"].as<std::string>() << std::endl;
           
           return TemplatedConstructor<int>(c["value"].as<std::string>());
       }
       if (c.hasOwnProperty("isNumber")){
           std::cout << "hasOwnProperty isNumber" << std::endl;
           return TemplatedConstructor<int>("wrapped constructor");
       }
       return TemplatedConstructor<int>("wrapped constructor");
    }))
    .function("set", &TemplatedConstructor<int>::set)
    .function("get", &TemplatedConstructor<int>::get);
}

blockspacer avatar Nov 09 '18 14:11 blockspacer

This issue has been automatically marked as stale because there has been no activity in the past year. It will be closed automatically if no further activity occurs in the next 7 days. Feel free to re-open at any time if this issue is still relevant.

stale[bot] avatar Nov 09 '19 15:11 stale[bot]

Hello ! I'd love to have the feature mentionned in the first post, without the big workaround suggested below :)

FelixNumworks avatar Mar 21 '25 09:03 FelixNumworks

There's a way to do this now with std::optional from the PR #23675.

#include <optional>

using namespace emscripten;

struct Rectangle {
  int x = 0;
  int y = 0;
  int width = 100;
  int height = 100;
  std::optional<int> color = 0xFF000000;
};

EMSCRIPTEN_BINDINGS(shapes) {
  register_optional<int>();
  value_object<Rectangle>("Rectangle")
    .field("x", &Rectangle::x)
    .field("y", &Rectangle::y)
    .field("width", &Rectangle::width)
    .field("height", &Rectangle::height)
    .field("color", &Rectangle::color);

  class_<Shape>("Shape")
    .constructor<Rectangle>();
}

brendandahl avatar Mar 21 '25 15:03 brendandahl

Though the default value won't work.

brendandahl avatar Mar 21 '25 15:03 brendandahl

Another option if you want a default value is to use a getter/setter with optional, but leave the Rectangle class alone. e.g.

struct Rectangle {
  int x = 0;
  int y = 0;
  int width = 100;
  int height = 100;
  int color = 0xFF000000;
};
...
    .field("color",
      +[](const Rectangle& r) -> std::optional<int> {
        return r.color;
      },
      +[](Rectangle& r, std::optional<int> color) {
        r.color = color.value_or(42);
      }
    )

brendandahl avatar Mar 21 '25 15:03 brendandahl