Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,12 @@ module Container(T)
values.each { |v| self << v }
self
end

# Wrapper selection macro
macro of(*type_args) end

# General conversion method (§2.6.1)
# abstract def self.from(values : Enumerable(T)) : Container(T)
end
```

Expand All @@ -585,6 +591,79 @@ appear in method argument types or return types; explicit instantiations may be
configured with the `containers` section. Aliases to complete container types
and container type arguments are both supported.

It is possible to store containers as Crystal instance variables using the
container module. However, the generic module does not name the actual wrapper
type instantiated by Bindgen. To do this, the `.of` macro should be used:

```crystal
class Foo
# `Container(Int32)` is the module type
# `Container.of(Int32)` produces the concrete, non-generic wrapper type
# corresponding to the `Int32` instantiation
# For all `T`, `Container.of(T) < Container(T)`, and no other types include
# the same instantiated module
@x : Container(Int32) = Container.of(Int32).new

# Referring to a non-instantiated type is a compilation error
# @x = Container.of(NoReturn).new

# If the wrapper type is stored in a constant, it can be used in array literal
# initializers
BitArray = Container.of(Bool)
SECRET = BitArray{true, false, false, true}

# Type restrictions, like instance variables, can only use module types due to
# grammar restrictions
def matches_secret?(bits : Container(Bool))
bits.to_a == SECRET.to_a
end

# Module types can be nested
def transpose(ary : Container(Container(Float64)))
Container.of(Container(Float64)).new
end

# One level of nested `Enumerable`s can be automatically wrapped in an array
# literal initializer
FloatMatrix = Container.of(Container(Float32))
def skew(x : Float32, y : Float32, z : Float32)
FloatMatrix{
[0_f32, -z, y],
[ z, 0_f32, -x],
[ -y, x, 0_f32],
}
end
end
```

### §2.6.1 Automatic container conversion

Every container wrapper class defines a `.from` class method, which takes any
matching enumerable and converts it into that wrapper type. Enumerables passed
to C++ are automatically converted using the same method. In both cases, the
argument is reused if it is already of the wrapper type.

```cpp
struct Foo {
static double sum(const Container<double> &values);
};
```

```crystal
class Foo
def self.sum(values : Enumerable(Float64)) : Float64
# converted = Container.of(Float64).from(values)
# ...
end
end

Foo.sum([1.2, 3.4]) # => 4.6
Foo.sum({1.2, 3.4}) # => 4.6

# No copies are generated
Foo.sum(Container.of(Float64).from [1.2, 3.4]) # => 4.6
```

## §3. Crystal bindings

### §3.1 Naming scheme
Expand Down
23 changes: 18 additions & 5 deletions assets/glue.cr
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,11 @@ module BindgenHelper
end

# Wraps a *list* into a container *wrapper*, if it's not already one.
macro wrap_container(wrapper, list)
%instance = {{ list }}
if %instance.is_a?({{ wrapper }})
%instance
def self.wrap_container(wrapper : T.class, list) : T forall T
if list.is_a?(T)
list
else
{{wrapper}}.new.concat(%instance)
wrapper.new.concat(list)
end
end

Expand Down Expand Up @@ -119,5 +118,19 @@ module BindgenHelper
to_a.inspect(io)
io << ">"
end

# Note: Crystal has no dependent types, so this module must refer to its own
# type parameter `T`.
private module ClassMethods(T)
# Wraps the list of *values* into a container of this type, if it's not
# already one.
def from(values : Enumerable(T)) : self
BindgenHelper.wrap_container(self, values)
end
end

macro included
extend ClassMethods(T)
end
end
end
16 changes: 16 additions & 0 deletions spec/integration/containers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,20 @@ class Containers {

return d;
}

std::vector<std::vector<double>> transpose(const std::vector<std::vector<double>> &mat) {
std::size_t height = mat.size();
std::size_t width = mat[0].size();

std::vector<std::vector<double>> trsp;
for (std::size_t x = 0; x < width; ++x) {
std::vector<double> row;
for (std::size_t y = 0; y < height; ++y) {
row.push_back(mat[y][x]);
}
trsp.push_back(std::move(row));
}

return trsp;
}
};
15 changes: 14 additions & 1 deletion spec/integration/containers_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ require "./spec_helper"
describe "container instantiation feature" do
it "works" do
build_and_run("containers") do
FloatMatrix = Test::Std::Vector.of(Test::Std::Vector(Float64))

context "sequential container" do
it "works with explicitly instantiated container" do
Test::Containers.new.integers.to_a.should eq([1, 2, 3])
Expand All @@ -15,6 +17,11 @@ describe "container instantiation feature" do
it "works with auto instantiated container (argument)" do
list = [1.5, 2.5]
Test::Containers.new.sum(list).should eq(4.0)
Test::Containers.new.sum(Test::Std::Vector.of(Float64).from(list)).should eq(4.0)

list = {3.5, 4.5}
Test::Containers.new.sum(list).should eq(8.0)
Test::Containers.new.sum(Test::Std::Vector.of(Float64).from(list)).should eq(8.0)
end

it "works with auto instantiated container (aliased container)" do
Expand All @@ -26,7 +33,13 @@ describe "container instantiation feature" do
end

it "works with nested containers" do
Test::Containers.new.grid.to_a.map(&.to_a).should eq([[1, 4], [9, 16]])
Test::Containers.new.grid.map(&.to_a).should eq([[1, 4], [9, 16]])

mat = FloatMatrix{
[1.2, 3.4],
[5.6, 7.8],
}
Test::Containers.new.transpose(mat).map(&.to_a).should eq([[1.2, 5.6], [3.4, 7.8]])
end
end
end
Expand Down
73 changes: 73 additions & 0 deletions src/bindgen/call_builder/crystal_container_of.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
module Bindgen
module CallBuilder
# Builder for the `.of` macro of container modules.
class CrystalContainerOf
def initialize(@db : TypeDatabase)
end

def build(method : Parser::Method, container : Configuration::Container) : Call
raise "not a macro" unless method.macro?

result = Call::Result.new(
type: Parser::Type::EMPTY,
type_name: "",
reference: false,
pointer: 0,
)

Call.new(
origin: method,
name: method.name,
arguments: [] of Call::Argument,
result: result,
body: Body.new(@db, container),
)
end

class Body < Call::Body
def initialize(@db : TypeDatabase, @container : Configuration::Container)
end

def to_code(call : Call, _platform : Graph::Platform) : String
%[macro #{call.name}(*type_args)\n] \
%[#{macro_body}\n] \
%[end]
end

private def macro_body
instantiations = @container.instantiations.map do |inst|
inst.map { |t| @db.resolve_aliases(t).full_name }
end.uniq

if instantiations.empty?
return %[ {% raise "\#{self} has no instantiations" %}]
end

pass = Crystal::Pass.new(@db)
typer = Crystal::Typename.new(@db)
cpp_typer = Cpp::Typename.new

branches = instantiations.map_with_index do |inst, i|
type_name = cpp_typer.template_class(@container.class, inst)
templ_type = Parser::Type.parse(type_name)
templ_args = templ_type.template.not_nil!.arguments

klass_name = "Container_#{templ_type.mangled_name}"
arg_list = templ_args.join(", ") do |t|
@db.try_or(t, typer.full(pass.to_wrapper(t), expects_type: false), &.container_type)
end

%[ {% #{i == 0 ? "if" : "elsif"} types == {#{arg_list}} %} {{ #{klass_name} }}\n]
end

module_name = Graph::Path.from(@container.class).last_part.camelcase

%[ {% types = type_args.map(&.resolve) %}\n] \
%[#{branches.join}] \
%[ {% else %} {% raise "#{module_name}(\#{types.splat}) has not been instantiated" %}\n] \
%[ {% end %}]
end
end
end
end
end
17 changes: 13 additions & 4 deletions src/bindgen/crystal/typename.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@ module Bindgen
def initialize(@db : TypeDatabase)
end

# Returns the full Crystal type-name of *result*.
def full(result : Call::Expression)
# Returns the full Crystal type-name of *result*. If *expects_type* is
# false, the type-name may appear in regular code, which affects how
# pointers are formatted.
def full(result : Call::Expression, *, expects_type = true)
ptr = result.pointer
ptr += 1 if result.reference
stars = "*" * ptr
nilable = "?" if result.nilable?
"#{result.type_name}#{stars}#{nilable}"

if expects_type
stars = "*" * ptr if ptr > 0
"#{result.type_name}#{stars}#{nilable}"
else
prefix = "Pointer(" * ptr if ptr > 0
suffix = ")" * ptr if ptr > 0
"#{prefix}#{result.type_name}#{suffix}#{nilable}"
end
end

# The type-name of *type* for use in a wrapper.
Expand Down
41 changes: 29 additions & 12 deletions src/bindgen/graph/path.cr
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ module Bindgen
end
end

# Returns a copy of this path with every part CamelCased.
def camelcase
Path.new(@parts.map(&.camelcase), global?)
end

# Gives the path as Crystal constants look-up path.
def to_s(io)
io << "::" if global?
Expand All @@ -115,14 +120,16 @@ module Bindgen
end
end

# Returns a new `Path` on *path*. Supports generic parts.
def self.from(path : String) : Path
# Returns a new `Path` on *path*. All generic type arguments are removed,
# unless *generic* is true.
def self.from(path : String, *, generic : Bool = false) : Path
if path == ""
self_path
elsif path == "::"
global_root
else
parts = path.gsub(Util::BALANCED_PARENS_RX, "").split("::")
path = remove_generics(path) unless generic
parts = path.split("::")
if global = parts.first?.try(&.empty?)
parts.shift
end
Expand All @@ -132,22 +139,27 @@ module Bindgen
end

# :ditto:
def self.from(path : Path) : Path
new(path.parts.dup, path.global?)
def self.from(path : Path, *, generic : Bool = false) : Path
if generic
new(path.parts.dup, path.global?)
else
new(path.parts.map(&->remove_generics(String)), path.global?)
end
end

# Returns a new `Path` formed by concatenating the given paths. An empty
# collection produces a self-path.
def self.from(paths : Enumerable(String | Path)) : Path
# Returns a new `Path` formed by concatenating the given *paths*. Each
# path in the collection may correspond to more than one namespace level;
# an empty collection produces a self-path.
def self.from(paths : Enumerable(String | Path), *, generic : Bool = false) : Path
paths.reduce(self_path) do |path, other|
path.join!(other.is_a?(Path) ? other : from(other))
path.join!(from(other, generic: generic))
end
end

# :ditto:
def self.from(first_path : String | Path, *remaining : String | Path) : Path
remaining.reduce(from(first_path)) do |path, other|
path.join!(other.is_a?(Path) ? other : from(other))
def self.from(first_path : String | Path, *remaining : String | Path, generic : Bool = false) : Path
remaining.reduce(from(first_path, generic: generic)) do |path, other|
path.join!(from(other, generic: generic))
end
end

Expand Down Expand Up @@ -266,6 +278,11 @@ module Bindgen
nil # Not found.
end

# Removes the generic types from the given *path*.
private def self.remove_generics(path)
path.gsub(Util::BALANCED_PARENS_RX, "")
end

# Finds the last common element in the lists *a* and *b*, and returns it.
private def self.last_common(a, b)
found = nil
Expand Down
8 changes: 7 additions & 1 deletion src/bindgen/parser/method.cr
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ module Bindgen
# Qt signal
Signal

# Crystal macro. Only internally used by Bindgen to indicate that a
# macro is none of the other method types.
Macro

# Is this one of the constructors?
def any_constructor? : Bool
constructor? || aggregate_constructor? || copy_constructor?
Expand Down Expand Up @@ -127,7 +131,7 @@ module Bindgen
delegate constructor?, aggregate_constructor?, copy_constructor?,
any_constructor?, member_method?, member_getter?, member_setter?,
static?, static_method?, static_getter?, static_setter?, signal?,
operator?, conversion_operator?, destructor?, to: @type
operator?, conversion_operator?, destructor?, macro?, to: @type
delegate public?, protected?, private?, to: @access

def_equals_and_hash @type, @name, @class_name, @access, @arguments,
Expand Down Expand Up @@ -328,6 +332,8 @@ module Bindgen
"clone"
when .destructor?
"finalize"
when .macro?
name
else
raise "BUG: No #crystal_name implementation for type #{@type}"
end
Expand Down
Loading