diff --git a/SPEC.md b/SPEC.md index 6dd231d..452769b 100644 --- a/SPEC.md +++ b/SPEC.md @@ -250,10 +250,12 @@ if: * The member is not an lvalue or rvalue reference * The type's `copy_structure` is not set * The type's `instance_variable` settings do not reject the member +* If the member is a C array, only complete arrays of C/C++ built-in types are + allowed The getter is always defined for every instance variable, but the setter is -omitted if the instance variable was defined as a `const` member. Property -methods are underscored. +omitted if the instance variable was defined as a C array or `const` member. +Property methods are underscored. ```cpp struct Point { @@ -346,6 +348,31 @@ class OutParam end ``` +### §2.4.3 Array properties + +Getters are generated for complete C arrays of built-in types, which return +`Slice`s instead of Crystal `Array`s. The `Slice`s are marked as read-only if +the array member is `const`. Multi-dimensional arrays are also supported. + +```cpp +struct Foo { + int bar[15][20][25]; + void *baz[8]; + const float quux[2]; + Foo *children[3]; +}; +``` + +```crystal +class Foo + def bar : Slice(Int32[25][20]) end + def baz : Slice(Void*) end + def quux : Slice(Float32) end # read_only: true + + # `#children` is not defined +end +``` + ## §3. Crystal bindings ### §3.1 Naming scheme diff --git a/assets/bindgen_helper.hpp b/assets/bindgen_helper.hpp index 22ee985..0919d91 100644 --- a/assets/bindgen_helper.hpp +++ b/assets/bindgen_helper.hpp @@ -62,6 +62,7 @@ static __attribute__((noreturn)) void bindgen_fatal_panic(const char *message) { #ifdef __cplusplus #include #include +#include // Break C++'s encapsulation to allow easy wrapping of protected methods. #define protected public @@ -74,6 +75,18 @@ static std::string bindgen_crystal_to_stdstring(CrystalString str) { return std::string(str.ptr, str.size); } +/* Wrapper for a Crystal `Slice`. Layout-compatible to `Slice`. */ +struct CrystalSlice { + int32_t size; + bool read_only; + const void *pointer; +}; + +template +/*constexpr*/ CrystalSlice bindgen_array_to_slice(T (&arr)[N]) noexcept { + return CrystalSlice{ static_cast(N), std::is_const::value, arr }; +} + /* Wrapper for a Crystal `Proc`. */ template struct CrystalProc { diff --git a/assets/glue.cr b/assets/glue.cr index 92ff636..c3c0284 100644 --- a/assets/glue.cr +++ b/assets/glue.cr @@ -50,8 +50,9 @@ lib Binding # Container for raw memory-data. The `ptr` could be anything. struct CrystalSlice + size : Int32 + read_only : Bool ptr : Void* - size : LibC::Int end end @@ -74,6 +75,32 @@ module BindgenHelper ) end + # Wraps `Slice` to a `Binding::CrystalSlice`, which can then pass on to C++. + def self.wrap_slice(slice : Slice(T)) forall T + Binding::CrystalSlice.new( + size: slice.size, + read_only: slice.read_only?, + ptr: slice.to_unsafe.as(Pointer(Void)), + ) + end + + # Wraps `Slice` to a `Binding::CrystalSlice`, which can then pass on to C++. + # `Nil` version, returns an empty slice. + def self.wrap_slice(nothing : Nil) forall T + Binding::CrystalSlice.new( + size: 0, + read_only: true, + ptr: Pointer(Void).null, + ) + end + + # Unwraps a `Binding::CrystalSlice` to a Crystal `Slice(T)`. + module SliceConverter(T) + def self.unwrap(slice : Binding::CrystalSlice) + Slice(T).new(slice.ptr.as(Pointer(T)), slice.size, read_only: slice.read_only) + end + end + # Wraps a *list* into a container *wrapper*, if it's not already one. macro wrap_container(wrapper, list) %instance = {{ list }} diff --git a/clang/include/structures.hpp b/clang/include/structures.hpp index 5e2ed71..4e967c8 100644 --- a/clang/include/structures.hpp +++ b/clang/include/structures.hpp @@ -15,6 +15,7 @@ struct Type { bool isReference = false; // If this is a reference bool isBuiltin = false; // If this is a C++ built-in type. bool isVoid = false; // If the derefenced type is C++ `void`. + std::vector extents; // C array extents, e.g. `int[][5][3]` => `{0, 5, 3}` std::string baseName; // Base type. E.g., `const Foo *&` => `Foo` std::string fullName; // Full name, for C++. E.g. `const Foo *&` @@ -25,7 +26,7 @@ JsonStream &operator<<(JsonStream &s, const Type &value); struct Template { std::string fullName; // The template class, e.g. `std::vector<_Tp, _Alloc>` in `std::vector` - std::string baseName; // The template class-name, e.g. `std::vector` + std::string baseName; // The template class-name, e.g. `std::vector` std::vector arguments; // Arguments, e.g. `std::string` }; @@ -97,6 +98,8 @@ struct Method { CopyConstructor, // Destructor, MemberMethod, + // MemberGetter, + // MemberSetter, StaticMethod, Operator, // Overloaded operator Signal, // Qt signal diff --git a/clang/src/structures.cpp b/clang/src/structures.cpp index 5927a85..dbc74e6 100644 --- a/clang/src/structures.cpp +++ b/clang/src/structures.cpp @@ -10,6 +10,7 @@ static JsonStream &writeTypeJson(JsonStream &s, const Type &value) { << std::make_pair("isBuiltin", value.isBuiltin) << c << std::make_pair("isVoid", value.isVoid) << c << std::make_pair("pointer", value.pointer) << c + << std::make_pair("extents", value.extents) << c << std::make_pair("baseName", value.baseName) << c << std::make_pair("fullName", value.fullName) << c << std::make_pair("template", value.templ); diff --git a/clang/src/type_helper.cpp b/clang/src/type_helper.cpp index ac24a50..d44498f 100644 --- a/clang/src/type_helper.cpp +++ b/clang/src/type_helper.cpp @@ -32,6 +32,24 @@ void TypeHelper::qualTypeToType(Type &target, const clang::QualType &qt, clang:: target.fullName = ClangTypeName::getFullyQualifiedName(qt, ctx); } + if (const auto *arr = ctx.getAsArrayType(qt)) { + // Do not support `T [static 4]` or `T [*]` for now + if (arr->getSizeModifier() == clang::ArrayType::ArraySizeModifier::Normal) { + // DependentSizedArrayType may be in class templates; assume it doesn't exist + // Do not support VariableArrayType for now + if (const auto *carr = llvm::dyn_cast(arr)) { + target.pointer++; + target.extents.push_back(carr->getSize().getZExtValue()); + return qualTypeToType(target, carr->getElementType(), ctx); // Recurse + } + else if (const auto *iarr = llvm::dyn_cast(arr)) { + target.pointer++; + target.extents.push_back(0); + return qualTypeToType(target, iarr->getElementType(), ctx); // Recurse + } + } + } + if (qt->isReferenceType() || qt->isPointerType()) { target.isReference = target.isReference || qt->isReferenceType(); target.isMove = target.isMove || qt->isRValueReferenceType(); diff --git a/spec/bindgen/parser/type_spec.cr b/spec/bindgen/parser/type_spec.cr index 995ef98..fb3c0b5 100644 --- a/spec/bindgen/parser/type_spec.cr +++ b/spec/bindgen/parser/type_spec.cr @@ -6,26 +6,33 @@ end describe Bindgen::Parser::Type do describe "#decayed" do - it "returns nil for base type" do # Rule 4 + it "returns nil for base type" do # Rule 5 parse("int").decayed.should be_nil end - it "decays pointer depth" do # Rule 3 + it "decays pointer depth" do # Rule 4 parse("int **").decayed.should eq(parse("int *")) parse("int *").decayed.should eq(parse("int")) end - it "decays reference" do # Rule 2 + it "decays reference" do # Rule 3 parse("int *&").decayed.should eq(parse("int **")) parse("int &").decayed.should eq(parse("int *")) end - it "decays const" do # Rule 1 + it "decays const" do # Rule 2 parse("const int *&").decayed.should eq(parse("int *&")) parse("const int &").decayed.should eq(parse("int &")) parse("const int *").decayed.should eq(parse("int *")) parse("const int").decayed.should eq(parse("int")) end + + it "decays arrays" do # Rule 1 + parse("int [4]").decayed.should eq(parse("int")) + parse("int *[4]").decayed.should eq(parse("int *")) + parse("int [4][3]").decayed.should eq(parse("int [3]")) + parse("int *[4][3]").decayed.should eq(parse("int *[3]")) + end end describe "#parse" do @@ -101,6 +108,82 @@ describe Bindgen::Parser::Type do type.full_name.should eq("const int **") end + it "recognizes 'int [4]'" do + type = parse("int [4]") + type.const?.should be_false + type.reference?.should be_false + type.extents.should eq([4] of UInt64) + type.pointer.should eq(1) + type.base_name.should eq("int") + type.full_name.should eq("int [4]") + end + + it "recognizes 'int[4]'" do + type = parse("int[4]") + type.const?.should be_false + type.reference?.should be_false + type.extents.should eq([4] of UInt64) + type.pointer.should eq(1) + type.base_name.should eq("int") + type.full_name.should eq("int[4]") + end + + it "recognizes 'int []'" do + type = parse("int []") + type.const?.should be_false + type.reference?.should be_false + type.extents.should eq([0] of UInt64) + type.pointer.should eq(1) + type.base_name.should eq("int") + type.full_name.should eq("int []") + end + + it "recognizes 'int [4][3][2]'" do + type = parse("int [4][3][2]") + type.const?.should be_false + type.reference?.should be_false + type.extents.should eq([4, 3, 2] of UInt64) + type.pointer.should eq(3) + type.base_name.should eq("int") + type.full_name.should eq("int [4][3][2]") + end + + it "recognizes 'int [][3][2]'" do + type = parse("int [][3][2]") + type.const?.should be_false + type.reference?.should be_false + type.extents.should eq([0, 3, 2] of UInt64) + type.pointer.should eq(3) + type.base_name.should eq("int") + type.full_name.should eq("int [][3][2]") + end + + it "recognizes 'const int [4]'" do + type = parse("const int [4]") + type.const?.should be_true + type.reference?.should be_false + type.extents.should eq([4] of UInt64) + type.pointer.should eq(1) + type.base_name.should eq("int") + type.full_name.should eq("const int [4]") + end + + it "recognizes 'int * [4]'" do + type = parse("int * [4]") + type.const?.should be_false + type.reference?.should be_false + type.extents.should eq([4] of UInt64) + type.pointer.should eq(2) + type.base_name.should eq("int") + type.full_name.should eq("int * [4]") + end + + pending "recognizes 'int (&) [4]'" do + end + + pending "recognizes 'int [4] *'" do + end + it "supports pointer depth offset" do parse("int", 1).pointer.should eq(1) parse("int *", 1).pointer.should eq(2) diff --git a/spec/clang/functions_spec.cr b/spec/clang/functions_spec.cr index e589b82..dcea733 100644 --- a/spec/clang/functions_spec.cr +++ b/spec/clang/functions_spec.cr @@ -1,7 +1,7 @@ require "./spec_helper" -describe "clang tool macros feature" do - it "exports the macros" do +describe "clang tool functions feature" do + it "exports the C functions" do clang_tool( %[ void simple(); diff --git a/spec/integration/basic.cpp b/spec/integration/basic.cpp index 80d13a4..73a8f13 100644 --- a/spec/integration/basic.cpp +++ b/spec/integration/basic.cpp @@ -55,3 +55,13 @@ class TypeConversion { struct ImplicitConstructor { int itWorks() { return 1; } }; + +struct PlainStruct { + int x; + int *xp; + int y[4]; + int *yp[6]; + int z[8][16]; + void *vp; + void **vpp; +}; diff --git a/spec/integration/basic.yml b/spec/integration/basic.yml index a57ac34..ad1442e 100644 --- a/spec/integration/basic.yml +++ b/spec/integration/basic.yml @@ -6,6 +6,7 @@ classes: Adder: AdderWrap ImplicitConstructor: ImplicitConstructor TypeConversion: TypeConversion + PlainStruct: PlainStruct types: IgnoreMe: # Ignore by type @@ -18,3 +19,7 @@ types: wrapper_pass_by: Value to_crystal: "String.new(%)" from_crystal: "%.to_unsafe" + PlainStruct: + generate_binding: false + generate_wrapper: false + copy_structure: true diff --git a/spec/integration/basic_spec.cr b/spec/integration/basic_spec.cr index 688ad7e..64aabf7 100644 --- a/spec/integration/basic_spec.cr +++ b/spec/integration/basic_spec.cr @@ -40,6 +40,21 @@ describe "a basic C++ wrapper" do end end + context "copy structure" do + it "supports normal members" do + Test::Binding::PlainStruct.new.x.should be_a(Int32) + Test::Binding::PlainStruct.new.xp.should be_a(Pointer(Int32)) + Test::Binding::PlainStruct.new.vp.should be_a(Pointer(Void)) + Test::Binding::PlainStruct.new.vpp.should be_a(Pointer(Pointer(Void))) + end + + it "supports array members" do + Test::Binding::PlainStruct.new.y.should be_a(StaticArray(Int32, 4)) + Test::Binding::PlainStruct.new.yp.should be_a(StaticArray(Pointer(Int32), 6)) + Test::Binding::PlainStruct.new.z.should be_a(StaticArray(StaticArray(Int32, 16), 8)) + end + end + context "type decay matching" do it "supports specialized matching" do subject = Test::TypeConversion.new @@ -51,11 +66,11 @@ describe "a basic C++ wrapper" do subject = Test::TypeConversion.new subject.next(5u8).should eq(6u8) end - end - it "returns a void pointer" do - subject = Test::TypeConversion.new - subject.void_pointer.address.should eq(0x11223344) + it "returns a void pointer" do + subject = Test::TypeConversion.new + subject.void_pointer.address.should eq(0x11223344) + end end end end diff --git a/spec/integration/instance_properties.cpp b/spec/integration/instance_properties.cpp index 462ac4c..b0cd8ba 100644 --- a/spec/integration/instance_properties.cpp +++ b/spec/integration/instance_properties.cpp @@ -1,4 +1,5 @@ struct Point { + Point() { } Point(int x, int y) : x(x), y(y) { } int x, y; @@ -13,6 +14,9 @@ class Props { { (void)x_priv; // silence -Wunused-private-field (void)y_priv; + + v[2] = 2001; + v2[4][3][2] = 2002; } int x_pub; @@ -20,6 +24,15 @@ class Props { Point *position_ptr = new Point(12, 34); Point position_val {13, 35}; + int v[4] = { }; + int v2[5][6][7] = { }; + const int v_c[8] = {2003}; + int *v_ptr[9] = { }; + int *v2_ptr[10][11] = { }; + + Point points[2]; + Point *points_ptr[2]; + protected: int x_prot; const int y_prot; diff --git a/spec/integration/instance_properties_spec.cr b/spec/integration/instance_properties_spec.cr index bb77956..92f022a 100644 --- a/spec/integration/instance_properties_spec.cr +++ b/spec/integration/instance_properties_spec.cr @@ -63,6 +63,36 @@ describe "C++ instance properties" do position.x.should eq(13) position.y.should eq(35) end + + it "supports array members" do + props = Test::Props.new(5, 8) + + v = props.v + v.should be_a(Slice(Int32)) + v.size.should eq(4) + v.read_only?.should be_false + v[2].should eq(2001) + v[2] = 0 + v[2].should eq(0) + + v2 = props.v2 + v2.should be_a(Slice(Int32[7][6])) + v2.size.should eq(5) + v2.read_only?.should be_false + v2[4][3][2].should eq(2002) + + v_c = props.v_c + v_c.read_only?.should be_true + v_c[0].should eq(2003) + + props.v_ptr.should be_a(Slice(Int32*)) + props.v2_ptr.should be_a(Slice(Int32*[11])) + end + + it "is ignored for arrays of user types" do + {{ Test::Props.has_method?("points") }}.should be_false + {{ Test::Props.has_method?("points_ptr") }}.should be_false + end end context "setter methods" do @@ -114,6 +144,12 @@ describe "C++ instance properties" do got.x.should eq(60) got.y.should eq(61) end + + it "is ignored for array members" do + {% for member in %w[v= v2= v_c= v_ptr= v2_ptr= points= points_ptr=] %} + {{ Test::Props.has_method?(member) }}.should be_false + {% end %} + end end context "YAML configuration" do diff --git a/src/bindgen/cpp/pass.cr b/src/bindgen/cpp/pass.cr index cd565b2..3a4bf41 100644 --- a/src/bindgen/cpp/pass.cr +++ b/src/bindgen/cpp/pass.cr @@ -30,8 +30,22 @@ module Bindgen end end + # Returns the type name of *type*, ignoring conversion templates. + def type_name_base(type : Parser::Type) : String + if type.c_array? + # Note: `CrystalSlice` was supposed to be a template type, but + # `extern "C"` functions cannot return template types, so the element + # type is always erased. + "CrystalSlice" + elsif type.kind.function? + crystal_proc_name(type) + else + type.base_name + end + end + # Returns the type name of *proc_type*. - def crystal_proc_name(proc_type : Parser::Type) : String + private def crystal_proc_name(proc_type : Parser::Type) : String typer = Typename.new inner_args = proc_type.template.not_nil!.arguments @@ -67,8 +81,7 @@ module Bindgen ptr = type_pointer_depth(type) pass_by = TypeDatabase::PassBy::Original - type_name = type.base_name - type_name = crystal_proc_name(type) if type.kind.function? + type_name = type_name_base(type) # If the method expects a value, but we don't copy its structure, we pass # a reference to it instead. @@ -121,11 +134,9 @@ module Bindgen is_ref = type.reference? ptr = type_pointer_depth(type) is_val = type.pointer < 1 - generate_template = false pass_by = TypeDatabase::PassBy::Original - type_name = type.base_name - type_name = crystal_proc_name(type) if type.kind.function? + type_name = type_name_base(type) # TODO: Check for copy-constructor. if (is_constructor || is_val) && is_copied @@ -137,14 +148,19 @@ module Bindgen is_ref = false ptr = 1 - generate_template = !is_copied pass_by = TypeDatabase::PassBy::Pointer + elsif type.c_array? + type = type.remove_all_extents + is_ref = false + ptr = 0 + pass_by = TypeDatabase::PassBy::Value + template = "bindgen_array_to_slice(%)" end template = Template::None.new if rules = @db[type]? - template = rules.from_cpp + template ||= rules.from_cpp type_name = rules.cpp_type || type_name pass_by = rules.pass_by unless rules.pass_by.original? is_ref, ptr = reconfigure_pass_type(pass_by, is_ref, ptr) @@ -185,8 +201,7 @@ module Bindgen # type to the outside, as received by C++ (Thus even ignoring # `rules.cpp_type`!). It still follows the passing rules towards Crystal. def passthrough_to_crystal(type : Parser::Type) - type_name = type.base_name - type_name = crystal_proc_name(type) if type.kind.function? + type_name = type_name_base(type) to_cr = to_crystal(type, is_constructor: false) Call::Result.new( @@ -209,8 +224,7 @@ module Bindgen # Passes the *type* through without changes. def through(type : Parser::Type) - type_name = type.base_name - type_name = crystal_proc_name(type) if type.kind.function? + type_name = type_name_base(type) Call::Result.new( type: type, diff --git a/src/bindgen/cpp/typename.cr b/src/bindgen/cpp/typename.cr index ec192a7..d4c0a16 100644 --- a/src/bindgen/cpp/typename.cr +++ b/src/bindgen/cpp/typename.cr @@ -4,22 +4,22 @@ module Bindgen struct Typename # Formats the type in *result* in C++ style. def full(result : Call::Expression) : String - full(result.type_name, result.type.const?, result.pointer, result.reference) + full(result.type_name, result.type.const?, result.pointer, result.reference, result.type.extents) end # ditto - def full(base_name : String, const, pointer, is_reference) : String - stars = "*" * pointer - ref = "&" if is_reference - + def full(base_name : String, const, pointer, is_reference, extents) : String String.build do |b| b << "const " if const b << base_name - if pointer > 0 || is_reference - b << ' ' << ("*" * pointer) - b << '&' if is_reference - end + stars = "*" * pointer if pointer > 0 + subscripts = extents.map {|v| "[#{v unless v.zero?}]"}.join unless + extents.nil? || extents.empty? + ref = "&" if is_reference + + b << ' ' if stars || ref || subscripts + b << stars << subscripts << ref end end diff --git a/src/bindgen/crystal/pass.cr b/src/bindgen/crystal/pass.cr index af3681c..126b489 100644 --- a/src/bindgen/crystal/pass.cr +++ b/src/bindgen/crystal/pass.cr @@ -144,8 +144,8 @@ module Bindgen # # If *qualified* is `true`, the type is assumed to be used outside the # `lib Binding`, and will be qualified if required. - def from_binding(type : Parser::Type, qualified = false, is_constructor = false) : Call::Result - from(type) do |is_ref, ptr, type_name, nilable| + def from_binding(type : Parser::Type, qualified = false, is_constructor = false, no_slice = false) : Call::Result + from(type, is_constructor, no_slice) do |is_ref, ptr, type_name, nilable| typer = Typename.new(@db) if qualified @@ -156,9 +156,17 @@ module Bindgen template = Template::None.new + if type.c_array? && !no_slice + is_ref = false + ptr = 0 + inner_result = from_binding(type.decayed.not_nil!, no_slice: true) + template = "BindgenHelper::SliceConverter(#{typer.full(inner_result)}).unwrap(%)" + type_name = "Binding::CrystalSlice" + end + if rules = @db[type]? unless is_constructor - template = type_template(rules.converter, rules.to_crystal, "unwrap") + template ||= type_template(rules.converter, rules.to_crystal, "unwrap") end is_ref, ptr = reconfigure_pass_type(rules.pass_by, is_ref, ptr) @@ -169,8 +177,8 @@ module Bindgen end # Computes a result for passing *type* from the wrapper to the user. - def from_wrapper(type : Parser::Type, is_constructor = false) : Call::Result - from(type) do |is_ref, ptr, type_name, nilable| + def from_wrapper(type : Parser::Type, is_constructor = false, no_slice = false) : Call::Result + from(type, is_constructor, no_slice) do |is_ref, ptr, type_name, nilable| typer = Typename.new(@db) local_type_name, in_lib = typer.wrapper(type) type_name = typer.qualified(local_type_name, in_lib) @@ -187,6 +195,13 @@ module Bindgen nilable = false end + if type.c_array? && !no_slice + is_ref = false + ptr = 0 + inner_result = from_wrapper(type.decayed.not_nil!, no_slice: true) + type_name = "Slice(#{typer.full(inner_result)})" + end + if !rules.builtin && !is_constructor && !rules.converter && rules.to_crystal.no_op? && !in_lib && !rules.kind.enum? template = wrapper_initialize_template(rules, type_name, nilable) end @@ -201,7 +216,7 @@ module Bindgen end end - def from(type : Parser::Type, is_constructor = false) : Call::Result + def from(type : Parser::Type, is_constructor = false, no_slice = false) : Call::Result is_copied = is_type_copied?(type) is_ref = type.reference? is_val = type.pointer < 1 @@ -221,6 +236,8 @@ module Bindgen # Hand-off is_ref, ptr, type_name, template, nilable = yield is_ref, ptr, type_name, nilable ptr += 1 if is_ref # Translate reference to pointer + ptr -= type.extents.size + type = type.remove_all_extents unless no_slice Call::Result.new( type: type, diff --git a/src/bindgen/crystal/typename.cr b/src/bindgen/crystal/typename.cr index b493e2f..cd33810 100644 --- a/src/bindgen/crystal/typename.cr +++ b/src/bindgen/crystal/typename.cr @@ -6,13 +6,18 @@ module Bindgen def initialize(@db : TypeDatabase) end - # Returns the full Crystal type-name of *result*. + # Returns the full Crystal type-name of *result*. The type-name cannot be + # used in normal code if the type contains pointers or arrays. def full(result : Call::Expression) ptr = result.pointer ptr += 1 if result.reference stars = "*" * ptr nilable = "?" if result.nilable? - "#{result.type_name}#{stars}#{nilable}" + + extents = result.type.extents.reverse + subscripts = extents.map {|v| "[#{v unless v.zero?}]"}.join + + "#{result.type_name}#{stars}#{subscripts}#{nilable}" end # The type-name of *type* for use in a wrapper. diff --git a/src/bindgen/generator/crystal_lib.cr b/src/bindgen/generator/crystal_lib.cr index 771ba5f..57b97cc 100644 --- a/src/bindgen/generator/crystal_lib.cr +++ b/src/bindgen/generator/crystal_lib.cr @@ -33,13 +33,23 @@ module Bindgen def visit_struct(structure) puts "struct #{structure.name}" indented do - structure.fields.each do |name, type| - # can't use Void as a struct field type - if type.type_name == "Void" - puts "#{name} : #{type.type_name}*" - else - puts "#{name} : #{type.type_name}" + structure.fields.each do |name, result| + # can't use Void as a struct field type directly + ptr = result.pointer + ptr = {ptr, 1}.max if result.type_name == "Void" + + if result.type.c_array? + # Crystal's `Int32[2][3][4]` is really equivalent to C's + # `int [4][3][2]`, so the extents have to be reversed when they + # are written to a `lib`. + extents = result.type.extents.reverse + subscripts = extents.map {|v| "[#{v}]"}.join + ptr -= extents.size end + + stars = "*" * ptr if ptr > 0 + + puts "#{name} : #{result.type_name}#{stars}#{subscripts}" end end puts "end" diff --git a/src/bindgen/parser/argument.cr b/src/bindgen/parser/argument.cr index d6049ac..3669550 100644 --- a/src/bindgen/parser/argument.cr +++ b/src/bindgen/parser/argument.cr @@ -16,6 +16,10 @@ module Bindgen isBuiltin: Bool, isVoid: Bool, pointer: Int32, + extents: { + type: Array(UInt64), + default: Array(UInt64).new, + }, baseName: String, fullName: String, nilable: { @@ -42,7 +46,8 @@ module Bindgen def initialize( @name, @baseName, @fullName, @isConst, @isReference, @isMove, @isBuiltin, @isVoid, @pointer, @kind = Type::Kind::Class, @hasDefault = false, - @value = nil, @nilable = false, @isVariadic = false + @value = nil, @nilable = false, @isVariadic = false, + @extents = Array(UInt64).new ) end @@ -56,12 +61,15 @@ module Bindgen @isVoid = type.isVoid @isVariadic = false @pointer = type.pointer + @extents = type.extents @kind = type.kind @template = type.template @nilable = type.nilable end - def_equals_and_hash @baseName, @fullName, @isConst, @isReference, @isMove, @isBuiltin, @isVoid, @pointer, @hasDefault, @name, @value, @nilable + def_equals_and_hash @baseName, @fullName, @isConst, @isReference, @isMove, + @isBuiltin, @isVoid, @pointer, @hasDefault, @name, @value, @nilable, + @extents # Does this argument have a default value? def has_default? @@ -100,6 +108,7 @@ module Bindgen isBuiltin: @isBuiltin, isVoid: @isVoid, pointer: @pointer, + extents: extents, kind: @kind, hasDefault: false, value: nil, @@ -121,6 +130,7 @@ module Bindgen isBuiltin: @isBuiltin, isVoid: @isVoid, pointer: @pointer, + extents: extents, kind: @kind, hasDefault: @hasDefault || other.has_default?, value: @value || other.value, @@ -130,7 +140,7 @@ module Bindgen # Checks if the type-part of this equals the type-part of *other*. def type_equals?(other : Type) - {% for i in %i[baseName fullName isConst isReference isMove isBuiltin isVoid pointer template] %} + {% for i in %i[baseName fullName isConst isReference isMove isBuiltin isVoid pointer extents template] %} return false if @{{ i.id }} != other.{{ i.id }} {% end %} diff --git a/src/bindgen/parser/field.cr b/src/bindgen/parser/field.cr index ae5aa13..a0b21a3 100644 --- a/src/bindgen/parser/field.cr +++ b/src/bindgen/parser/field.cr @@ -14,6 +14,10 @@ module Bindgen isBuiltin: Bool, isVoid: Bool, pointer: Int32, + extents: { + type: Array(UInt64), + default: Array(UInt64).new, + }, baseName: String, fullName: String, nilable: { diff --git a/src/bindgen/parser/method.cr b/src/bindgen/parser/method.cr index cbf7869..a04c17b 100644 --- a/src/bindgen/parser/method.cr +++ b/src/bindgen/parser/method.cr @@ -384,6 +384,11 @@ module Bindgen return true if list.includes?(@name) end + # TODO: Support methods that take or return C arrays (which become + # `Slice` in Crystal). + return true if @returnType.c_array? + return true if @arguments.any?(&.c_array?) + # Check that all arguments, which pass in explicit by-value, either take # the value directly, or a const-reference to it. pass_by_value_violation = @arguments.any? do |arg| diff --git a/src/bindgen/parser/type.cr b/src/bindgen/parser/type.cr index de9e4fc..24fbb98 100644 --- a/src/bindgen/parser/type.cr +++ b/src/bindgen/parser/type.cr @@ -14,8 +14,10 @@ module Bindgen Function end - # ATTENTION: Changes here have to be kept in sync with `Parser::Argument`s mapping!! - # Also make sure to update other methods in here and in `Argument` as required! + # ATTENTION: Changes here have to be kept in sync with `Parser::Argument` + # and `Parser::Field`'s mapping!! + # Also make sure to update other methods in here and in those two classes + # as required! JSON.mapping( kind: { type: Kind, @@ -27,6 +29,10 @@ module Bindgen isBuiltin: Bool, isVoid: Bool, pointer: Int32, + extents: { + type: Array(UInt64), + default: Array(UInt64).new, + }, baseName: String, fullName: String, nilable: { @@ -48,6 +54,7 @@ module Bindgen isBuiltin: true, isVoid: true, pointer: 0, + extents: Array(UInt64).new, baseName: "void", fullName: "void", template: nil, @@ -63,6 +70,7 @@ module Bindgen isBuiltin: true, isVoid: false, pointer: 0, + extents: Array(UInt64).new, baseName: "", fullName: "", template: nil, @@ -78,6 +86,7 @@ module Bindgen isBuiltin: true, isVoid: (cpp_name == "void"), pointer: pointer, + extents: Array(UInt64).new, baseName: cpp_name, fullName: cpp_name, template: nil, @@ -104,6 +113,14 @@ module Bindgen name = name[0..-2] # Remove ampersand end + # Is it a C array? + extents = [] of UInt64 + while subscript = name.match(/\s*\[\s*(\d*)\s*\]\s*$/) + extents.unshift subscript[1].to_u64 { 0_u64 } + pointer_depth += 1 + name = subscript.pre_match + end + # Is it a pointer? while name.ends_with?('*') pointer_depth += 1 @@ -111,12 +128,13 @@ module Bindgen end new( # Build the `Type` -isConst: const, + isConst: const, isMove: false, isReference: reference, isBuiltin: false, # Oh well isVoid: (name == "void"), pointer: pointer_depth, + extents: extents, baseName: name.strip, fullName: type_name, template: nil, @@ -139,13 +157,14 @@ isConst: const, ) new( # Build the `Type` -kind: Kind::Function, + kind: Kind::Function, isConst: false, isMove: false, isReference: false, isBuiltin: false, isVoid: false, pointer: 0, + extents: Array(UInt64).new, baseName: base, fullName: base, template: template, @@ -157,27 +176,32 @@ kind: Kind::Function, # removed from this type. Each rule is tried in the following order. # The first winning rule returns a new type. # - # 1. If `#const?`, remove const (`const int &` -> `int &`) - # 2. If `#reference?`, pointer-ize (`int &` -> `int *`) - # 3. If `#pointer > 0`, remove one (`int *` -> `int`) - # 4. Else, it's the base-type already. Return `nil`. + # 1. If `#c_array?`, remove outermost extent (`int [4][3]` -> `int [3]`) + # 2. If `#const?`, remove const (`const int &` -> `int &`) + # 3. If `#reference?`, pointer-ize (`int &` -> `int *`) + # 4. If `#pointer > 0`, remove one (`int *` -> `int`) + # 5. Else, it's the base-type already. Return `nil`. def decayed : Type? is_const = @isConst is_ref = @isReference ptr = @pointer + extents = @extents - if is_const # 1. + if extents.size > 0 # 1. + extents = extents[1..] + ptr -= 1 + elsif is_const # 2. is_const = false - elsif is_ref # 2. + elsif is_ref # 3. is_ref = false - elsif ptr > 0 # 3. + elsif ptr > 0 # 4. ptr -= 1 - else # 4. + else # 5. return nil end typer = Cpp::Typename.new - type_ptr = ptr + type_ptr = ptr - extents.size type_ptr -= 1 if is_ref Type.new( @@ -188,8 +212,9 @@ kind: Kind::Function, isBuiltin: @isBuiltin, isVoid: @isVoid, pointer: ptr, + extents: extents, baseName: @baseName, - fullName: typer.full(@baseName, is_const, type_ptr, is_ref), + fullName: typer.full(@baseName, is_const, type_ptr, is_ref, extents), template: @template, nilable: @nilable, ) @@ -207,6 +232,7 @@ kind: Kind::Function, isBuiltin: @isBuiltin, isVoid: @isVoid, pointer: @pointer, + extents: extents, baseName: @baseName, fullName: @fullName, template: @template, @@ -215,9 +241,14 @@ kind: Kind::Function, end end - def_equals_and_hash @baseName, @fullName, @isConst, @isReference, @isMove, @isBuiltin, @isVoid, @pointer, @kind, @nilable + def_equals_and_hash @baseName, @fullName, @isConst, @isReference, @isMove, + @isBuiltin, @isVoid, @pointer, @kind, @nilable, @extents - def initialize(@baseName, @fullName, @isConst, @isReference, @pointer, @isMove = false, @isBuiltin = false, @isVoid = false, @kind = Kind::Class, @template = nil, @nilable = false) + def initialize( + @baseName, @fullName, @isConst, @isReference, @pointer, @isMove = false, + @isBuiltin = false, @isVoid = false, @kind = Kind::Class, + @template = nil, @nilable = false, @extents = Array(UInt64).new + ) end # Is this type nilable? For compatibility with `Argument`. @@ -225,8 +256,8 @@ kind: Kind::Function, # Checks if this type equals the *other* type, except for nil-ability. def equals_except_nil?(other : Type) - {% for i in %i[baseName fullName isConst isReference isMove isBuiltin isVoid pointer kind] %} - return false if @{{ i.id }} != other.{{ i.id }} + {% for i in %i[baseName fullName isConst isReference isMove isBuiltin isVoid pointer kind extents] %} + return false if @{{ i.id }} != other.{{ i.id }} {% end %} true @@ -263,6 +294,46 @@ kind: Kind::Function, @isVoid && @pointer == 0 end + # The array extents of this type. + def extents + @extents.dup + end + + # Is this type a C array? + def c_array? + @extents.size > 0 + end + + # Is this type a C array with known size? + def c_complete_array? + c_array? && @extents.none?(&.zero?) + end + + # Returns a copy of this type with C array extents removed. + def remove_all_extents : Type + ptr = @pointer - @extents.size + extents = Array(UInt64).new + + typer = Cpp::Typename.new + type_ptr = ptr + type_ptr -= 1 if @isReference + + Type.new( + kind: @kind, + isConst: @isConst, + isReference: @isReference, + isMove: false, + isBuiltin: @isBuiltin, + isVoid: @isVoid, + pointer: ptr, + extents: extents, + baseName: @baseName, + fullName: typer.full(@baseName, @isConst, type_ptr, @isReference, extents), + template: @template, + nilable: @nilable, + ) + end + # Unqualified base name for easier mapping to Crystal. # # E.g., the base name of `const QWidget *&` is `QWidget`. diff --git a/src/bindgen/processor/copy_structs.cr b/src/bindgen/processor/copy_structs.cr index 78168c6..9db8be1 100644 --- a/src/bindgen/processor/copy_structs.cr +++ b/src/bindgen/processor/copy_structs.cr @@ -39,7 +39,7 @@ module Bindgen private def copy_structure(klass, root) typename = Crystal::Typename.new(@db) Graph::Struct.new( # Add the struct into the graph -name: typename.binding(klass.as_type).first.camelcase, + name: typename.binding(klass.as_type).first.camelcase, fields: fields_to_graph(klass.fields), parent: root, ) diff --git a/src/bindgen/processor/instance_properties.cr b/src/bindgen/processor/instance_properties.cr index 00e31a8..2b84e1a 100644 --- a/src/bindgen/processor/instance_properties.cr +++ b/src/bindgen/processor/instance_properties.cr @@ -20,6 +20,10 @@ module Bindgen # Ignore private fields. Also ignore all reference fields for now. next if field.private? || field.reference? || field.move? + # For C arrays only built-in types are supported; user types will need + # a custom `Slice`-like wrapper so that raw pointers are not returned. + next if field.c_array? && (!field.builtin? || !field.c_complete_array?) + pattern, config = lookup_member_config(var_config, field.name) next if config.ignore @@ -31,7 +35,7 @@ module Bindgen field_type = config.nilable ? (field.make_pointer_nilable || field) : field add_getter(klass, access, field_type, field.name, method_name) - add_setter(klass, access, field_type, field.name, method_name) unless field.const? + add_setter(klass, access, field_type, field.name, method_name) unless field.const? || field.c_array? end super diff --git a/src/bindgen/processor/sanity_check.cr b/src/bindgen/processor/sanity_check.cr index 971acfc..a11e3d7 100644 --- a/src/bindgen/processor/sanity_check.cr +++ b/src/bindgen/processor/sanity_check.cr @@ -143,6 +143,25 @@ module Bindgen super end + # Check for invalid types in structures + def visit_struct(structure) + structure.fields.each do |name, result| + if result.reference + add_error(structure, "Struct field #{name} is a reference") + end + + if result.type.c_array? + if !result.type.builtin? + add_error(structure, "Struct field #{name} is a C array of user type") + end + + if !result.type.c_complete_array? + add_error(structure, "Struct field #{name} is a C array with unknown bounds") + end + end + end + end + # Check reachability of all types private def visit_method(method) call = method.calls[@platform]? diff --git a/src/bindgen/processor/virtual_override.cr b/src/bindgen/processor/virtual_override.cr index adfe740..df01702 100644 --- a/src/bindgen/processor/virtual_override.cr +++ b/src/bindgen/processor/virtual_override.cr @@ -299,7 +299,7 @@ module Bindgen Call::Result.new( type: proc_type, - type_name: pass.crystal_proc_name(proc_type), + type_name: pass.type_name_base(proc_type), reference: false, pointer: 0, ) @@ -325,7 +325,7 @@ module Bindgen # Pass by reference. table_type = Parser::Type.new( baseName: table_name, - fullName: typer.full(table_name, const: false, pointer: 0, is_reference: true), + fullName: typer.full(table_name, const: false, pointer: 0, is_reference: true, extents: nil), isConst: true, isReference: true, pointer: 1,