Skip to content
This repository was archived by the owner on Feb 15, 2026. It is now read-only.
Merged
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
33 changes: 33 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,39 @@ class Point
end
```

### §2.6 Generic types

Only sequential container types are instantiated on the Crystal side, as if each
container implements the following interface:

```crystal
module Container(T)
include Indexable(T)

# All containers must be default-constructible
# abstract def initialize

abstract def unsafe_fetch(index : Int) : T
abstract def push(value : T) : Void
abstract def size : Int32

def <<(x : T)
push(x)
self
end

def concat(values : Enumerable(T))
values.each { |v| self << v }
self
end
end
```

Bindgen automatically collects all instantiations of each container type that
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.

## §3. Crystal bindings

### §3.1 Naming scheme
Expand Down
2 changes: 1 addition & 1 deletion clang/find_clang.cr
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ spec_base_content = {
output: "tmp/{SPEC_NAME}.cr",
},
},
library: "%/tmp/{SPEC_NAME}.o -lstdc++",
library: "%/tmp/{SPEC_NAME}.o -lstdc++ -lgccpp",
parser: {
files: ["{SPEC_NAME}.cpp"],
includes: [
Expand Down
11 changes: 11 additions & 0 deletions spec/integration/containers.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#include <vector>
#include <string>

typedef std::vector<unsigned char> bytearray;
typedef unsigned int rgb;

class Containers {
public:
std::vector<int> integers() {
Expand All @@ -15,6 +18,14 @@ class Containers {
return std::vector<std::string>{ "One", "Two", "Three" };
}

bytearray chars() {
return { 0x01, 0x04, 0x09 };
}

std::vector<rgb> palette() {
return { 0xFF0000, 0x00FF00, 0x0000FF };
}

double sum(std::vector<double> list) {
double d = 0;

Expand Down
7 changes: 5 additions & 2 deletions spec/integration/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

processors:
- filter_methods
- auto_container_instantiation
- instantiate_containers
- default_constructor
- cpp_wrapper
Expand All @@ -17,6 +18,8 @@ containers:
type: Sequential
instantiations:
- [ "int" ]
- [ "double" ]
- [ "std::string" ]
- [ "std::vector<int>" ]

types:
rgb: { alias_for: "unsigned int" }
bytearray: { alias_for: std::vector<unsigned char> }
8 changes: 8 additions & 0 deletions spec/integration/containers_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ describe "container instantiation feature" do
Test::Containers.new.sum(list).should eq(4.0)
end

it "works with auto instantiated container (aliased container)" do
Test::Containers.new.chars.to_a.should eq([1u8, 4u8, 9u8])
end

it "works with auto instantiated container (aliased element)" do
Test::Containers.new.palette.to_a.should eq([0xFF0000u32, 0x00FF00u32, 0x0000FFu32])
end

it "works with nested containers" do
Test::Containers.new.grid.to_a.map(&.to_a).should eq([[1, 4], [9, 16]])
end
Expand Down
2 changes: 1 addition & 1 deletion src/bindgen/configuration.cr
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ module Bindgen
property type : Type

# List of instantiations to create.
property instantiations : Array(Array(String)) = [] of Array(String)
property instantiations = Set(Array(String)).new

# Method to access an element at an index.
property access_method : String = "at"
Expand Down
6 changes: 2 additions & 4 deletions src/bindgen/cpp/method_name.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ module Bindgen
# Finds *class_name* in the graph and checks if it's shadow sub-classed in
# C++. If so, returns the name of the shadow class.
private def class_name_for_new(class_name)
if klass = @db.try_or(class_name, nil, &.graph_node.as(Graph::Class))
klass.cpp_sub_class || class_name
else
class_name
@db.try_or(class_name, class_name) do |rules|
rules.graph_node.as?(Graph::Class).try(&.cpp_sub_class)
end
end
end
Expand Down
14 changes: 8 additions & 6 deletions src/bindgen/processor/auto_container_instantiation.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ module Bindgen
m = method.origin

try_add_container_type m.return_type
try_add_container_type @db.resolve_aliases m.return_type
m.arguments.each do |argument|
try_add_container_type argument
try_add_container_type @db.resolve_aliases argument
end
end

Expand All @@ -28,17 +30,17 @@ module Bindgen
container = @config.containers.find(&.class.== templ.base_name)
return if container.nil? # Not a configured container type

# Check for the correct amount of template arguments. There may be more
# than those arguments, which are usually allocators.
# Check for the correct amount of template arguments. There may be more
# than those arguments, which are usually allocators.
arg_count = container_type_arguments(container.type)
return if templ.arguments.size < arg_count

# Add if we don't already know of this instantiation
instantiation = templ.arguments[0, arg_count].map(&.full_name)

unless container.instantiations.includes?(instantiation)
container.instantiations << instantiation
instantiation = templ.arguments[0...arg_count].map do |arg|
@db.resolve_aliases(arg).full_name
end

container.instantiations << instantiation
end

# Returns the count of template arguments expected for a container of
Expand Down
90 changes: 47 additions & 43 deletions src/bindgen/processor/instantiate_containers.cr
Original file line number Diff line number Diff line change
Expand Up @@ -37,46 +37,54 @@ module Bindgen

# Adds all instances of the sequential *container* into *root*.
private def add_sequential_containers(container, root)
container.instantiations.each do |instance|
resolve_instantiations(container).each do |instance|
check_sequential_instance! container, instance
add_sequential_container(container, instance, root)
end
end

# Resolves aliases in the type arguments of *container*'s instantiations.
# This is required because aliases from the config files are not resolved
# prior to this point.
private def resolve_instantiations(container)
container.instantiations.map do |inst|
inst.map { |t| @db.resolve_aliases(t).full_name }
end.uniq
end

# Instantiates a single *container* *instance* into *root*.
private def add_sequential_container(container, instance, root)
builder = Graph::Builder.new(@db)
var_type = Parser::Type.parse(instance.first)
klass = build_sequential_class(container, var_type)

add_cpp_typedef(root, klass, container, instance)
set_sequential_container_type_rules(klass.name, klass, var_type)
templ_type = Parser::Type.parse(cpp_container_name(container, instance))
templ_args = templ_type.template.not_nil!.arguments
klass = build_sequential_class(container, templ_type)

add_cpp_typedef(root, templ_type, klass.name)
set_sequential_container_type_rules(klass, templ_type)

graph = builder.build_class(klass, klass.name, root)
graph.set_tag(Graph::Class::FORCE_UNWRAP_VARIABLE_TAG)
graph.included_modules << container_module(SEQUENTIAL_MODULE, var_type)
graph.included_modules << container_module(SEQUENTIAL_MODULE, templ_args)
end

# Generates the C++ template name of a container class.
private def cpp_container_name(container, instance)
typer = Cpp::Typename.new
typer.template_class(container.class, instance)
end

# Generates the Crystal module name of a container class.
private def container_module(kind, *types)
private def container_module(kind, types)
pass = Crystal::Pass.new(@db)
typer = Crystal::Typename.new(@db)
args = types.map { |t| typer.full pass.to_wrapper(t) }.join(", ")

"#{kind}(#{args})"
end

# Adds a `tyepedef Container<T...> Container_T...` for C++. Also stores
# the alias in the type-database.
private def add_cpp_typedef(root, klass, container, instance)
typer = Cpp::Typename.new
type = Parser::Type.parse(typer.template_class(container.class, instance))

# Alias e.g. `QList_QObject_X` to `QList<QObject *>`
if @db[type.base_name]?.nil?
@db.add_alias(type.base_name, klass.name)
end

# Adds a `typedef Container<T...> Container_T...` for C++.
private def add_cpp_typedef(root, type, cpp_type_name)
# On top for C++!
host = Graph::PlatformSpecific.new(platform: Graph::Platform::Cpp)
root.nodes.unshift host
Expand All @@ -91,25 +99,23 @@ module Bindgen

Graph::Alias.new( # Build the `typedef`.
origin: origin,
name: klass.name,
name: cpp_type_name,
parent: host,
)
end

# Updates the *rules* of the container *klass*, carrying a *var_type*.
# The rules are changed to convert from and to the binding type.
private def set_sequential_container_type_rules(cpp_type_name, klass : Parser::Class, var_type)
pass = Crystal::Pass.new(@db)

rules = @db.get_or_add(cpp_type_name)
result = pass.to_wrapper(var_type)
# Updates the rules of the sequential container *klass*, whose
# instantiated type is *templ_type*. The rules are changed to convert
# from and to the binding type.
private def set_sequential_container_type_rules(klass : Parser::Class, templ_type)
rules = @db.get_or_add(templ_type.full_name)
type_args = templ_type.template.not_nil!.arguments

rules.builtin = true # `Void` is built-in!
rules.pass_by = TypeDatabase::PassBy::Pointer
rules.wrapper_pass_by = TypeDatabase::PassBy::Value
rules.binding_type = "Void"
rules.crystal_type ||= "Enumerable(#{result.type_name})"
rules.cpp_type ||= cpp_type_name
rules.binding_type = klass.name
rules.crystal_type ||= container_module("Enumerable", type_args)
rules.cpp_type ||= klass.name

if rules.to_crystal.no_op?
rules.to_crystal = Template.from_string(
Expand All @@ -125,25 +131,26 @@ module Bindgen
if rules.to_cpp.no_op?
rules.to_cpp = Template.from_string(@db.cookbook.pointer_to_reference(klass.name))
end
end

# Name of *container* with *instance* for diagnostic purposes.
private def diagnostics_name(container, instance)
typer = Cpp::Typename.new
typer.template_class(container.class, instance)
# We can no longer mark a template specialization as an alias of another
# type, so we cheat by making both types share the same binding type
# (this is normally not an issue since all binding types are `Void`).
rules = @db.get_or_add(klass.as_type)
rules.binding_type = klass.name
end

# Checks if *instance* of *container* is valid. If not, raises.
private def check_sequential_instance!(container, instance)
if instance.size != 1
raise "Container #{diagnostics_name container, instance} was expected to have exactly one argument"
raise "Container #{container.class} was expected to have exactly one template argument"
end
end

# Builds a full `Parser::Class` for the sequential *container* in the
# specified *instantiation*.
private def build_sequential_class(container, var_type : Parser::Type) : Parser::Class
klass = container_class(container, {var_type})
private def build_sequential_class(container, templ_type : Parser::Type) : Parser::Class
var_type = templ_type.template.not_nil!.arguments.first
klass = container_class(container, templ_type)

klass.methods << default_constructor_method(klass)
klass.methods << access_method(container, klass.name, var_type)
Expand All @@ -158,11 +165,8 @@ module Bindgen
#
# Note: The returned class doesn't include any modules. This is done on
# the `Graph::Class` of the Crystal wrapper, see `#container_module`.
private def container_class(container, instantiation : Enumerable(Parser::Type)) : Parser::Class
suffix = instantiation.map(&.mangled_name).join("_")
klass_type = Parser::Type.parse(container.class)
name = "Container_#{klass_type.mangled_name}_#{suffix}"

private def container_class(container, templ_type : Parser::Type) : Parser::Class
name = "Container_#{templ_type.mangled_name}"
Parser::Class.new(name: name, has_default_constructor: true)
end

Expand Down
7 changes: 7 additions & 0 deletions src/bindgen/processor/sanity_check.cr
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ module Bindgen
# Regular expression describing a method name
METHOD_NAME_RX = /^[a-z_][A-Za-z0-9_]*[?!=]?$/

# Regular expression for a Crystal `Enumerable` typename
# TODO: support other Crystal stdlib types (which might not correspond to
# any C++ type at all)
ENUMERABLE_RX = /^Enumerable(?:\([A-Za-z0-9_():*]*\))?$/

# A binding error
struct Error
# The node this error occured at
Expand Down Expand Up @@ -189,6 +194,8 @@ module Bindgen
true
elsif @db.try_or(expr.type, false, &.builtin?)
true # Crystal built-in
elsif expr.type_name.matches?(ENUMERABLE_RX)
true # Containers
else # Do a full look-up otherwise
Graph::Path.from(expr.type_name).lookup(base) != nil
end
Expand Down
2 changes: 1 addition & 1 deletion src/bindgen/type_database.cr
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ module Bindgen
while type
decayed_type = type.decayed

if found = @types[resolve_aliases(type.full_name).full_name]?
if found = @types[resolve_aliases(type).full_name]?
if decayed_type && (parent = @types[decayed_type.full_name]?)
found = parent.merge(found)
end
Expand Down