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
31 changes: 29 additions & 2 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions assets/bindgen_helper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ static __attribute__((noreturn)) void bindgen_fatal_panic(const char *message) {
#ifdef __cplusplus
#include <gc/gc_cpp.h>
#include <string>
#include <type_traits>

// Break C++'s encapsulation to allow easy wrapping of protected methods.
#define protected public
Expand All @@ -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<typename T, std::size_t N>
/*constexpr*/ CrystalSlice bindgen_array_to_slice(T (&arr)[N]) noexcept {
return CrystalSlice{ static_cast<uint32_t>(N), std::is_const<T>::value, arr };
}

/* Wrapper for a Crystal `Proc`. */
template<typename T, typename ... Args>
struct CrystalProc {
Expand Down
29 changes: 28 additions & 1 deletion assets/glue.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 }}
Expand Down
5 changes: 4 additions & 1 deletion clang/include/structures.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<uint64_t> 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 *&`

Expand All @@ -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>`
std::string baseName; // The template class-name, e.g. `std::vector`
std::string baseName; // The template class-name, e.g. `std::vector`
std::vector<Type> arguments; // Arguments, e.g. `std::string`
};

Expand Down Expand Up @@ -97,6 +98,8 @@ struct Method {
CopyConstructor,
// Destructor,
MemberMethod,
// MemberGetter,
// MemberSetter,
StaticMethod,
Operator, // Overloaded operator
Signal, // Qt signal
Expand Down
1 change: 1 addition & 0 deletions clang/src/structures.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions clang/src/type_helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<clang::ConstantArrayType>(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<clang::IncompleteArrayType>(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();
Expand Down
91 changes: 87 additions & 4 deletions spec/bindgen/parser/type_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions spec/clang/functions_spec.cr
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
10 changes: 10 additions & 0 deletions spec/integration/basic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
5 changes: 5 additions & 0 deletions spec/integration/basic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ classes:
Adder: AdderWrap
ImplicitConstructor: ImplicitConstructor
TypeConversion: TypeConversion
PlainStruct: PlainStruct

types:
IgnoreMe: # Ignore by type
Expand All @@ -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
23 changes: 19 additions & 4 deletions spec/integration/basic_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading