diff --git a/SPEC.md b/SPEC.md index e0a6ee9..63ba982 100644 --- a/SPEC.md +++ b/SPEC.md @@ -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 diff --git a/clang/find_clang.cr b/clang/find_clang.cr index ab0dcc7..66ba9b2 100644 --- a/clang/find_clang.cr +++ b/clang/find_clang.cr @@ -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: [ diff --git a/spec/integration/containers.cpp b/spec/integration/containers.cpp index 73ae0f5..db9cd2a 100644 --- a/spec/integration/containers.cpp +++ b/spec/integration/containers.cpp @@ -1,6 +1,9 @@ #include #include +typedef std::vector bytearray; +typedef unsigned int rgb; + class Containers { public: std::vector integers() { @@ -15,6 +18,14 @@ class Containers { return std::vector{ "One", "Two", "Three" }; } + bytearray chars() { + return { 0x01, 0x04, 0x09 }; + } + + std::vector palette() { + return { 0xFF0000, 0x00FF00, 0x0000FF }; + } + double sum(std::vector list) { double d = 0; diff --git a/spec/integration/containers.yml b/spec/integration/containers.yml index 6479a5c..48256ff 100644 --- a/spec/integration/containers.yml +++ b/spec/integration/containers.yml @@ -2,6 +2,7 @@ processors: - filter_methods + - auto_container_instantiation - instantiate_containers - default_constructor - cpp_wrapper @@ -17,6 +18,8 @@ containers: type: Sequential instantiations: - [ "int" ] - - [ "double" ] - - [ "std::string" ] - [ "std::vector" ] + +types: + rgb: { alias_for: "unsigned int" } + bytearray: { alias_for: std::vector } diff --git a/spec/integration/containers_spec.cr b/spec/integration/containers_spec.cr index b7ae929..7dbbfb6 100644 --- a/spec/integration/containers_spec.cr +++ b/spec/integration/containers_spec.cr @@ -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 diff --git a/src/bindgen/configuration.cr b/src/bindgen/configuration.cr index 8ce23cd..c1a351c 100644 --- a/src/bindgen/configuration.cr +++ b/src/bindgen/configuration.cr @@ -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" diff --git a/src/bindgen/cpp/method_name.cr b/src/bindgen/cpp/method_name.cr index 494ddbd..4cd0f2d 100644 --- a/src/bindgen/cpp/method_name.cr +++ b/src/bindgen/cpp/method_name.cr @@ -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 diff --git a/src/bindgen/processor/auto_container_instantiation.cr b/src/bindgen/processor/auto_container_instantiation.cr index fafc653..e8bba95 100644 --- a/src/bindgen/processor/auto_container_instantiation.cr +++ b/src/bindgen/processor/auto_container_instantiation.cr @@ -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 @@ -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 diff --git a/src/bindgen/processor/instantiate_containers.cr b/src/bindgen/processor/instantiate_containers.cr index f9ea876..7ab9120 100644 --- a/src/bindgen/processor/instantiate_containers.cr +++ b/src/bindgen/processor/instantiate_containers.cr @@ -37,28 +37,45 @@ 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(", ") @@ -66,17 +83,8 @@ module Bindgen "#{kind}(#{args})" end - # Adds a `tyepedef Container 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` - if @db[type.base_name]?.nil? - @db.add_alias(type.base_name, klass.name) - end - + # Adds a `typedef Container 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 @@ -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( @@ -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) @@ -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 diff --git a/src/bindgen/processor/sanity_check.cr b/src/bindgen/processor/sanity_check.cr index e5a4549..10342aa 100644 --- a/src/bindgen/processor/sanity_check.cr +++ b/src/bindgen/processor/sanity_check.cr @@ -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 @@ -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 diff --git a/src/bindgen/type_database.cr b/src/bindgen/type_database.cr index 382e48e..429fd6d 100644 --- a/src/bindgen/type_database.cr +++ b/src/bindgen/type_database.cr @@ -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