wingo / pictie

C++-to-webassembly testbed, in the form of a simple graphics library
Other
14 stars 0 forks source link

Use webidl binding generator #2

Open wingo opened 5 years ago

wingo commented 5 years ago

Pictie currently binds C++ to the web via the use of "embind", a Boost.Python-like library. See https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html. However there's a fair amount of magic involved in embind and it would be useful to contrast it with experience with the WebIDL binder, which takes a different approach to binding creation.

wingo commented 5 years ago

I defined a basic WebIDL file. Note that the Emscripten webidl bindings generator is stuck in the past, so the webidl language doesn't correspond with https://heycam.github.io/webidl/#idl-grammar.

interface Vector {
  void Vector(double x, double y);
  static Vector zero();
  [Value] Vector add([Const, Ref] Vector b);
  [Value] Vector sub([Const, Ref] Vector b);
  [Value] Vector scale(double scale);
  double magnitude();
  [Value] Vector normalize(double scale);
  [Value] Vector rotate90DegreesClockwise();

  readonly attribute double x;
  readonly attribute double y;
};

interface  Frame {
  void Frame([Const, Ref] Vector origin,
             [Const, Ref] Vector edge1,
             [Const, Ref] Vector edge2);
  [Value] Vector project([Const, Ref] Vector v);
  // FIXME: Apparently overloads must have the same return type??
  // [Value] Frame project([Const, Ref] Frame f);

  [Const, Value] readonly attribute Vector origin;
  [Const, Value] readonly attribute Vector edge1;
  [Const, Value] readonly attribute Vector edge2;
};

interface Color {
  void Color();
  void Color(byte gray);
  void Color(byte r, byte g, byte b);
  [Value] static Color black();
  [Value] static Color gray();
  [Value] static Color white();
  [Value] static Color blue();
  [Value] static Color red();

  readonly attribute byte r;
  readonly attribute byte g;
  readonly attribute byte b;
};

enum LineCapStyle {
  "LineCapStyle::Butt",
  "LineCapStyle::Square"
};

enum LineWidthScaling {
  "LineWidthScaling::Scaled",
  "LineWidthScaling::Unscaled",
};

interface DrawingContext {
  void DrawingContext(unsigned long resolution);
  void fill([Const, Ref] Color c);
  void drawTriangle([Const, Ref] Vector a,
                    [Const, Ref] Vector b,
                    [Const, Ref] Vector c,
                    [Const, Ref] Color color);
  void drawQuad([Const, Ref] Vector a,
                [Const, Ref] Vector b,
                [Const, Ref] Vector c,
                [Const, Ref] Vector d,
                [Const, Ref] Color color);
  void drawLine([Const, Ref] Vector a,
                [Const, Ref] Vector b,
                [Const, Ref] Color color,
                double width,
                LineCapStyle lineCapStyle);
  // sequence<Color> getPixels();
};

// Virtual interface.
interface Painter {
  void paint([Ref] DrawingContext cx, [Const, Ref] Frame frame);
  // Declare smart pointer?
};

/*
// How to declare top-level functions?
[SharedPtr] Painter triangle([Const, Ref] Vector a,
                             [Const, Ref] Vector b,
                             [Const, Ref] Vector c,
                             [Const, Ref] Color color);
[SharedPtr] Painter path([Vector, Const] Vector[] points,
                         [Const, Ref] Color color,
                         double width = 0.01,
                         LineCapStyle lineCapStyle = LineCapStyle::Butt,
                         LineWidthScaling widthScaling = LineWidthScaling::Unscaled);
// FIXME: unsigned long?
[SharedPtr] Painter image(unsigned long width, unsigned long height,
                          // FIXME: Move?
                          [Vector] Color[] pixels);

[SharedPtr] Painter transform([SharedPtr] Painter painter,
                              [Const, Ref] Vector origin,
                              [Const, Ref] Vector corner1,
                              [Const, Ref] Vector corner2);
[SharedPtr] Painter over([SharedPtr] Painter a, [SharedPtr] Painter b);

[SharedPtr] Painter parallelogram([Const, Ref] Vector origin,
                                  [Const, Ref] Vector edge1,
                                  [Const, Ref] Vector edge2,
                                  [Const, Ref] Color color);
[SharedPtr] Painter color([Const, Ref] Color color);
[SharedPtr] Painter flipHoriz([SharedPtr] Painter painter);
[SharedPtr] Painter flipVert([SharedPtr] Painter painter);
[SharedPtr] Painter rotate90([SharedPtr] Painter painter);
[SharedPtr] Painter rotate180([SharedPtr] Painter painter);
[SharedPtr] Painter rotate270([SharedPtr] Painter painter);
[SharedPtr] Painter beside([SharedPtr] Painter a, [SharedPtr] Painter b);
[SharedPtr] Painter below([SharedPtr] Painter a, [SharedPtr] Painter b);
[SharedPtr] Painter beside3([SharedPtr] Painter a,
                            [SharedPtr] Painter b,
                            [SharedPtr] Painter c);
[SharedPtr] Painter above3([SharedPtr] Painter a,
                           [SharedPtr] Painter b,
                           [SharedPtr] Painter c);
[SharedPtr] Painter black();
[SharedPtr] Painter gray();
[SharedPtr] Painter white();
[SharedPtr] Painter blue();
[SharedPtr] Painter red();
*/

On the plus side, the bindings generator runs quickly and generates small C++ and JS files. On the other hand though there are some limitations that prevent me from going further:

  1. The bindings generator doesn't know what a std::shared_ptr is. I annotated instances where shared_ptr is used as SharedPtr, in the extended attributes, but the webidl generator rightly only looks at extended attributes that it knows and ignores my SharedPtr annotations. So the result won't compile as-is.
  2. I couldn't see any way with this bindings generator to define functions at the top level, outside of a namespace or class. Hence the comments.
  3. The bindings generator doesn't know about std::vector, indicated by my [Vector] annotations. So it expects a raw pointer for these arrays.
  4. The bindings generator doesn't know about move semantics, which makes me unsure if passing the pixels to an image painter is going to do the right thing or if there's a double-destructor possibility.
  5. The functions that return classes with automatic storage (the ones marked with [Value] on the return type) return the value as... a static singleton :/ Probably the right short-term thing to do here would be to make the generated wrapper malloc a result.

On the other hand, the generated C++ and JavaScript is very simple and understandable, unlike whatever it is that embind does. So I think it can be a useful path to pursue webidl binding generation in the future, but probably taking the opportunity to update the tool to the latest WebIDL, probably by importing and replacing it with something from the WebIDL project.