Papierkorb / bindgen

Binding and wrapper generator for C/C++ libraries
GNU General Public License v3.0
179 stars 18 forks source link

Generic pseudo-instantiation macro for container wrapper types #102

Open HertzDevil opened 3 years ago

HertzDevil commented 3 years ago

Currently, all instantiated containers must be referred to using their mangled names. This PR provides two ways to name those wrapper types:

In Qt5.cr only QList and QVector have been wrapped so far. The .of macro for the latter looks like this:

module QVector(T)
  macro of(*type_args)
    {% types = type_args.map(&.resolve) %}
    {% if types == {UInt32} %} {{ Container_QVector_QRgb }}     # macro expansion here turns
    {% elsif types == {Point} %} {{ Container_QVector_QPoint }} # Path into fully qualified TypeNode
    {% elsif types == {PointF} %} {{ Container_QVector_QPointF }}
    {% elsif types == {Float64} %} {{ Container_QVector_qreal }}
    {% elsif types == {UInt32} %} {{ Container_QVector_unsigned_int }}
    {% elsif types == {Float64} %} {{ Container_QVector_double }}
    {% elsif types == {TextLength} %} {{ Container_QVector_QTextLength }}
    {% elsif types == {TextFormat} %} {{ Container_QVector_QTextFormat }}
    {% elsif types == {LineF} %} {{ Container_QVector_QLineF }}
    {% elsif types == {Line} %} {{ Container_QVector_QLine }}
    {% elsif types == {RectF} %} {{ Container_QVector_QRectF }}
    {% elsif types == {Rect} %} {{ Container_QVector_QRect }}
    {% else %} {% raise "QVector(#{types.splat}) has not been instantiated" %}
    {% end %}
  end
end

For the special case of sequential containers, these wrappers also gain a .from constructor, which does exactly the same thing as BindgenHelper.wrap_container, except again no mangled names are used (see below for an example). The .of mechanism is independent of sequential containers.

There are at least two issues that need to be addressed in future PRs:

Papierkorb commented 3 years ago

In general, I really like these macros. They're much nicer to use than those mumbo-jumbo generated ones! However:

def f(pen : Qt::Pen)
  # before
  pen.dash_pattern = [3.0, 1.5, 1.0, 0.5]
  # after
  pen.dash_pattern = Qt::QVector.of(Float64).from [3.0, 1.5, 1.0, 0.5]
end

I think the 'after' usage is too complicated. Bindgen strives to produce code that's nice to use to the lib-user (At the expense of ease-of-use as configuration goes at times).

HertzDevil commented 3 years ago

Understandable. That said, the breaking change only happens when passing containers to the wrapper; the wrapper should be free to return containers directly, i.e.

class Pen
  # before
  def dash_pattern : Enumerable(Float64)
  # after
  def dash_pattern : QVector(Float64)
end

This might be sufficient to fix the second issue. For it not to break existing code QVector(Float64) < Enumerable(Float64) must be true, but this isn't the case right now because Indexable(Float64) comes from BindgenHelper::SequentialContainer(Float64) instead, and namespaces (modules) cannot include other modules at the moment. So I'll leave this part as is and the patch is ready.

HertzDevil commented 3 years ago

QVector.of will look like this after this patch is rebased onto #108:

module QVector(T)
  macro of(*type_args)
    {% types = type_args.map(&.resolve) %}
    {% if types == {UInt32} %} {{ Container_QVector_unsigned_int_ }}
    {% elsif types == {Point} %} {{ Container_QVector_QPoint_ }}
    {% elsif types == {PointF} %} {{ Container_QVector_QPointF_ }}
    {% elsif types == {Float64} %} {{ Container_QVector_double_ }}
    {% elsif types == {TextLength} %} {{ Container_QVector_QTextLength_ }}
    {% elsif types == {TextFormat} %} {{ Container_QVector_QTextFormat_ }}
    {% elsif types == {LineF} %} {{ Container_QVector_QLineF_ }}
    {% elsif types == {Line} %} {{ Container_QVector_QLine_ }}
    {% elsif types == {RectF} %} {{ Container_QVector_QRectF_ }}
    {% elsif types == {Rect} %} {{ Container_QVector_QRect_ }}
    {% else %} {% raise "QVector(#{types.splat}) has not been instantiated" %}
    {% end %}
  end
end
HertzDevil commented 3 years ago

Rebased on top of master. Also there is now special logic to prefer container module names (TypeConfig#container_type) over #crystal_type, so nested container names can be disambiguated in the .of macro without the pass behaviour of any containers being altered (see last commit for examples).

Please take a look and see if anything still needs to be changed.

Papierkorb commented 3 years ago

This might be sufficient to fix the second issue. For it not to break existing code QVector(Float64) < Enumerable(Float64) must be true, but this isn't the case right now because Indexable(Float64) comes from BindgenHelper::SequentialContainer(Float64) instead, and namespaces (modules) cannot include other modules at the moment. So I'll leave this part as is and the patch is ready.

Couldn't this be done if each specialization would include these modules by themselves?

HertzDevil commented 3 years ago

More specifically it could break code that depends on that return type for type deduction, e.g. instance variables: https://play.crystal-lang.org/#/r/9z1p

If T.f returns an Enumerable(Int32) then any of the includes will work, but if it returns Vector(Int32) then only the one include inside Vector itself will work, because T#@x's type is deduced to be whatever T.f's return type restriction is. Thus it needs to be the case that Vector(T) < Enumerable(T).

Papierkorb commented 3 years ago

Mh, this works fine in the playground: https://play.crystal-lang.org/#/r/9zap