diff --git a/Project.toml b/Project.toml index 6517fde..ea7bb53 100644 --- a/Project.toml +++ b/Project.toml @@ -5,10 +5,12 @@ authors = ["Tim Holy and contributors"] [deps] Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" [compat] Graphs = "1" +JSON = "0.21.4" OrderedCollections = "1.8.1" julia = "1.12" diff --git a/src/JuliaLibWrapping.jl b/src/JuliaLibWrapping.jl index df160c7..62b922b 100644 --- a/src/JuliaLibWrapping.jl +++ b/src/JuliaLibWrapping.jl @@ -3,10 +3,10 @@ module JuliaLibWrapping using OrderedCollections using Graphs -export parselog, wrapper -export CProject +export import_abi_info, wrapper +export CProject, ABIInfo -include("parselog.jl") +include("abi_import.jl") include("c.jl") end diff --git a/src/abi_import.jl b/src/abi_import.jl new file mode 100644 index 0000000..b5e6f30 --- /dev/null +++ b/src/abi_import.jl @@ -0,0 +1,209 @@ +using JSON + +struct FieldDesc + name::String + type::Int + offset::Int +end + +struct PrimitiveTypeDesc + # TODO: support custom primitive types? + name::String +end + +struct StructDesc + name::String + size::Int + alignment::Int + fields::Vector{FieldDesc} +end + +struct PointerDesc + name::String + pointee_type::Int # type of pointee +end + +struct ArgDesc + name::String + type::Int + isva::Bool # is this a varargs argument? +end + +struct MethodDesc + symbol::String # exported C symbol + name::String # full method name w/ args + return_type::Int + args::Vector{ArgDesc} +end + +const TypeDesc = Union{StructDesc, PointerDesc, PrimitiveTypeDesc} + +function from_json(::Type{PrimitiveTypeDesc}, type::Dict{String,Any}) + return PrimitiveTypeDesc(type["name"]) +end + +function from_json(::Type{StructDesc}, type::Dict{String, Any}) + return StructDesc( + type["name"], + type["size"], + type["alignment"], + FieldDesc[ + FieldDesc( + field["name"], + field["type"], + field["offset"] + ) + for field in type["fields"] + ] + ) +end + +function from_json(::Type{PointerDesc}, json::Dict{String, Any}) + return PointerDesc(json["name"], json["pointee"]) +end + +function from_json(::Type{TypeDesc}, json::Dict{String, Any}) + kind = json["kind"]::String + if kind === "primitive" + return from_json(PrimitiveTypeDesc, json) + elseif kind === "struct" + return from_json(StructDesc, json) + elseif kind === "pointer" + return from_json(PointerDesc, json) + else # unreachable + @assert false "unexpected kind '$(json["kind"])' in type metadata" + end +end + +function from_json(::Type{MethodDesc}, method::Dict{String, Any}) + return MethodDesc( + method["symbol"], + method["name"], + method["returns"]["type"], + ArgDesc[ + ArgDesc( + arg["name"], + arg["type"], + #= isva =# false + ) + for arg in method["arguments"] + ], + ) +end + +function build_type_graph(typedescs::OrderedDict{Int, TypeDesc}; + pointer_filter::Function) + g = SimpleDiGraph(length(typedescs)) + for (id, desc) in pairs(typedescs) + if desc isa StructDesc + for field in desc.fields + add_edge!(g, field.type, id) + end + elseif desc isa PointerDesc + if pointer_filter(id) + add_edge!(g, desc.pointee_type, id) + else + # pointee types don't affect data layout (no edges to add) + end + elseif desc isa PrimitiveTypeDesc + # dependency is tracked from the `struct` side + end + end + return g +end + +""" + sort_declarations!(typedescs) -> forward_declarations + +Sort `typedescs` w.r.t. type-dependencies (e.g. Type A using Type B in a field), +so that a type-descriptor always appears after any dependencies. Sort is performed +in-place. + +Returns indices of all types that could not be sorted (due to recursive types, e.g. +a linked list in C). In C, these are the definitions that must be forward-declared. +""" +function sort_declarations!(typedescs::OrderedDict{Int, TypeDesc}) + # First we have to identify the parts of the graph where we have type-recursion and + # therefore have to use forward-declarations instead of just sorting declarations. + recursive_types = BitSet() + + full_type_graph = build_type_graph(typedescs; pointer_filter = (id)->true) + for scc in strongly_connected_components(full_type_graph) + length(scc) == 1 && continue + for type_id in scc + push!(recursive_types, type_id) + end + end + + # Now that we know the recursive types, we can restrict them from treating their pointers + # as a type dependency and re-build a type-graph that is acyclic (these deleted dependencies + # will later become a forward-declaration) + type_graph = build_type_graph(typedescs; pointer_filter = (id)->!in(id, recursive_types)) + + # We now have an acyclic type-dependency graph, and we have to emit our declarations in a + # topological order. This guarantees that if a type A has a dependency on type B, type B + # will appear before type A. + order_to_emit = zeros(length(typedescs)) + for (pos, desc_id) in enumerate(topological_sort(type_graph)) + # poor man's permute!(typedescs, topological_sort(g)) + order_to_emit[desc_id] = pos + end + sort!(typedescs; by=(id)->order_to_emit[id]) + + # Finally, we just need to compute the set of required forward declarations. + forwarddecls = BitSet() + for id in recursive_types + desc = typedescs[id] + desc isa StructDesc || continue + for field in desc.fields + dep = field.type + while typedescs[dep] isa PointerDesc + # 'dereference' any pointer type + dep = (typedescs[dep]::PointerDesc).pointee_type + end + order_to_emit[id] ≥ order_to_emit[dep] && continue + # this struct refers (through 1 or more pointers) to a type that will + # be emitted after it, so that type must be forward-declared. + push!(forwarddecls, dep) + end + end + + return forwarddecls +end + +struct ABIInfo + # A map from `type_id` to `type_descriptor`, sorted by declaration order. + typeinfo::OrderedDict{Int, TypeDesc} + # Indexes into `types`, indicates which types must be forward-declared for C. + forward_declared::BitSet + # A vector of exposed functions from the imported ABI + entrypoints::Vector{MethodDesc} +end + +""" + entrypoints, typedescs, forward_declarations = parselog(filename::String) + +Extract the signatures of the entrypoints and types from an exported ABI info (JSON) file. +""" +function import_abi_info(filename::String) + abi_info = JSON.Parser.parsefile(filename) + + # Extract all the type descriptors + typedescs = OrderedDict{Int, TypeDesc}() + for type in abi_info["types"] + id = Int(type["id"]::Int64) + typedescs[id] = from_json(TypeDesc, type) + end + + # Then collect the methods + entrypoints = MethodDesc[] + for method in abi_info["functions"] + push!(entrypoints, from_json(MethodDesc, method)) + end + + forward_declared = sort_declarations!(typedescs) + + return ABIInfo(typedescs, forward_declared, entrypoints) +end + +import_abi_info(filename::AbstractString) = import_abi_info(String(filename)::String) diff --git a/src/c.jl b/src/c.jl index 6363cd1..f4bf578 100644 --- a/src/c.jl +++ b/src/c.jl @@ -3,7 +3,16 @@ struct CProject headerbase::String end -function wrapper(dest::CProject, entrypoints, typedescs) +function unwrap_pointer_type(type_id::Int, typeinfo) + while typeinfo[type_id] isa PointerDesc + type_id = typeinfo[type_id].pointee_type + end + return type_id +end + +function wrapper(dest::CProject, abi_info::ABIInfo) + (; entrypoints, typeinfo, forward_declared) = abi_info + # Write the header file for C headerfile = joinpath(dest.dir, dest.headerbase * ".h") libvar = "JULIALIB_" * uppercase(dest.headerbase) * "_H" @@ -15,14 +24,36 @@ function wrapper(dest::CProject, entrypoints, typedescs) println(f, "#include ") println(f) - typedict = Dict{String, String}() - for type in values(typedescs) - println(f, "typedef struct {") - for field in type.fields - ft = mangle_c!(typedict, field.type) - println(f, " ", ft, " ", field.name, ";") - end - println(f, "} ", mangle_c!(typedict, type.name), ";") + typedict = Dict{Int, String}() + + # Print forward-declarations (if any, for recursive types) + for id in forward_declared + type = typeinfo[id] + @assert type isa StructDesc "un-expected forward declaration type" + mangled_name = mangle_c!(typedict, id, typeinfo) + println(f, "struct ", mangled_name, ";") + end + + # Print the struct definitions + printed = BitSet() + for (id, type) in pairs(typeinfo) + if type isa StructDesc + mangled_name = mangle_c!(typedict, id, typeinfo) + println(f, "typedef struct ", mangled_name, " {") + for field in type.fields + ft = mangle_c!(typedict, field.type, typeinfo) + if !in(unwrap_pointer_type(field.type, typeinfo), printed) + ft = "struct " * ft + end + println(f, " ", ft, " ", sanitize_for_c(field.name), ";") + end + println(f, "} ", mangled_name, ";") + elseif type isa PrimitiveTypeDesc + # We only rely on built-in primitive types (c.f. `ctypes`) + elseif type isa PointerDesc + # We emit pointer types in-line - no need for a separate typedef + else @assert false "unknown descriptor type" end + push!(printed, id) end println(f) @@ -31,7 +62,8 @@ function wrapper(dest::CProject, entrypoints, typedescs) if !isempty(args) args = ", " * args end - print(f, mangle_c!(typedict, method.return_type), " ", method.name, "(") + mangled_rt = mangle_c!(typedict, method.return_type, typeinfo) + print(f, mangled_rt, " ", method.symbol, "(") isfirst = true for arg in method.args if isfirst @@ -39,8 +71,8 @@ function wrapper(dest::CProject, entrypoints, typedescs) else print(f, ", ") end - ft = mangle_c!(typedict, arg.type) - print(f, ft, " ", arg.name) + ft = mangle_c!(typedict, arg.type, typeinfo) + print(f, ft, " ", sanitize_for_c(arg.name)) if arg.isva print(f, "...") end @@ -63,7 +95,16 @@ const ctypes = Dict{String, String}( "UInt64" => "uint64_t", "Float32" => "float", "Float64" => "double", - "Bool" => "_Bool", + "Bool" => "bool", + "RawFD" => "int", + + "Cstring" => "char *", + "Cwstring" => "wchar_t *", + + # Note: These types will never appear in an auto-exported ABI, since they are not + # distinct Julia types (these are just platform-specific aliases to the types above). + "Cchar" => "char", + "Cwchar_t" => "wchar_t", "Cvoid" => "void", "Cint" => "int", "Cshort" => "short", @@ -73,38 +114,45 @@ const ctypes = Dict{String, String}( "Culong" => "unsigned long", "Cssize_t" => "ssize_t", "Csize_t" => "size_t", - "Cchar" => "char", - "Cwchar_t" => "wchar_t", - "Cstring" => "char *", - "Cwstring" => "wchar_t *", - "RawFD" => "int", ) -function mangle_c!(typedict::Dict{String, String}, type::AbstractString) - ft = get(typedict, type, nothing) - ft !== nothing && return ft - ft = get(ctypes, type, nothing) - ft !== nothing && return ft - idxbad = findfirst(r"[^a-zA-Z0-9_\{\}]", type) - if idxbad !== nothing - error("Invalid type name: ", type, " (invalid character at position ", idxbad, ")") +function sanitize_for_c(str::AbstractString) + # Replace any non alphanumeric characters with '_' + str = replace(str, r"[^a-zA-Z0-9_]" => "_") + # Strip any leading / trailing underscores + str = strip(str, Char['_']) + # Merge any repeated underscores to just one + return replace(str, r"_+" => "_") +end + +function mangle_c!(typedict::Dict{Int, String}, type_id::Int, typeinfo::OrderedDict{Int,TypeDesc}) + if type_id in keys(typedict) + return typedict[type_id] end - m = match(r"^Ptr\{(.+)\}$", type) - if m !== nothing - inner_type = m.captures[1] - result = mangle_c!(typedict, inner_type) * "*" - typedict[type] = result - return result + + type = typeinfo[type_id] + if type isa PrimitiveTypeDesc + if !in(type.name, keys(ctypes)) + error("unsupported primitive type: '$(type.name)'") + end + return ctypes[type.name] + elseif type isa PointerDesc + mangled = mangle_c!(typedict, type.pointee_type, typeinfo) * "*" + elseif type isa StructDesc + mangled = sanitize_for_c(type.name) end - m = match(r"^(.+)\{(.+)\}$", type) - if m !== nothing - basename, params = m.captures - params = split(params, ",") - params = join(map(p -> mangle_c!(typedict, p), params), "_") - result = basename * "_" * params * "_" - typedict[type] = result - return result + + # Check for any name collision and unique the symbol, if necessary. + if mangled in values(typedict) + suffix = type_id + extended = mangled * "_" * string(suffix) + while extended in values(typedict) + suffix += 1 + extended = mangled * "_" * string(suffix) + end + mangled = extended end - typedict[type] = type - return type + + typedict[type_id] = mangled + return mangled end diff --git a/src/parselog.jl b/src/parselog.jl deleted file mode 100644 index c5a9bb4..0000000 --- a/src/parselog.jl +++ /dev/null @@ -1,129 +0,0 @@ -struct FieldDesc - name::String - type::String - offset::Int -end -function Base.show(io::IO, field::FieldDesc) - print(io, field.name, "::", field.type, "[", field.offset, "]") -end - -struct TypeDesc - name::String - fields::Vector{FieldDesc} - size::Int -end -function Base.show(io::IO, type::TypeDesc) - print(io, type.name, "(", join(type.fields, ", "), ") (", type.size, " bytes)") -end - -struct ArgDesc - name::String - type::String - isva::Bool # is this a varargs argument? -end -function Base.show(io::IO, arg::ArgDesc) - print(io, arg.name, "::", arg.type) - if arg.isva - print(io, "...") - end -end - -struct MethodDesc - name::String - return_type::String - args::Vector{ArgDesc} -end - -function Base.show(io::IO, method::MethodDesc) - print(io, method.name, "(", join(method.args, ", "), ")::", method.return_type) -end - -const rexsize = r"^(\d+) bytes$" -const rexfield = r"^ (.+)::(.+)\[(\d+)\]$" -const rexmethod = r"^(.+)\((.*)\)::(.+)$" -const rexarg = r"^(.+)::(.+)(\.\.\.)?$" - -""" - entrypoints, typedescs = parselog(filename::String) - -Extract the signatures of the entrypoints and nonstandard types from a log file. -""" -function parselog(filename::String) - entrypoints = MethodDesc[] - typedescs = OrderedDict{String, TypeDesc}() - - open(filename) do f - handling_types = false - while !eof(f) - line = rstrip(readline(f)) - if isempty(line) - handling_types = true - continue - elseif handling_types - current_type = line - fields = FieldDesc[] - line = rstrip(readline(f)) # Read the next line for type details - m = match(rexsize, line) - while m === nothing - m = match(rexfield, line) - field_name = m.captures[1] - field_type = m.captures[2] - field_offset = parse(Int, m.captures[3]) - push!(fields, FieldDesc(field_name, field_type, field_offset)) - line = rstrip(readline(f)) - m = match(rexsize, line) - end - type_size = parse(Int, m.captures[1]) - typedescs[current_type] = TypeDesc(current_type, fields, type_size) - else # methods - m = match(rexmethod, line) - method_name = m.captures[1] - args_str = m.captures[2] - return_type = m.captures[3] - - args = ArgDesc[] - for arg_str in split(args_str, ",") - arg_str = strip(arg_str) - if isempty(arg_str) - continue - end - m = match(rexarg, arg_str) - arg_name = m.captures[1] - arg_type = m.captures[2] - isva = length(m.captures) > 2 && m.captures[3] !== nothing # Check for varargs - push!(args, ArgDesc(arg_name, arg_type, isva)) - end - - push!(entrypoints, MethodDesc(method_name, return_type, args)) - end - end - end - - # Sort the types by building a dependency graph - g = SimpleDiGraph(length(typedescs)) - name2idx = Dict(name => i for (i, name) in enumerate(keys(typedescs))) - for (i, (_, type)) in enumerate(typedescs) - for field in type.fields - dep = get(name2idx, field.type, nothing) - if dep !== nothing - add_edge!(g, i, dep) - end - end - end - function lt(a, b) - # if neither is a declared type, compare by name - haskey(typedescs, a) || haskey(typedescs, b) || return a < b - # if one is not declared, it comes first - !haskey(typedescs, a) && return true - !haskey(typedescs, b) && return false - # otherwise, a < b if there is a path from b to a - ia, ib = name2idx[a], name2idx[b] - has_path(g, ib, ia) && return true - has_path(g, ia, ib) && return false - return a < b # if no path, compare by name - end - sort!(typedescs; lt) - - return entrypoints, typedescs -end -parselog(filename::AbstractString) = parselog(String(filename)::String) diff --git a/test/bindinginfo_libsimple.json b/test/bindinginfo_libsimple.json new file mode 100644 index 0000000..308f1c6 --- /dev/null +++ b/test/bindinginfo_libsimple.json @@ -0,0 +1,139 @@ +{ + "functions": [ + { + "symbol": "copyto_and_sum", + "name": "copyto_and_sum(fromto::CVectorPair{Float32})", + "arguments": [ + { "name": "fromto", "type": 1 } + ], + "returns": { "type": 2 } + }, + { + "symbol": "tree_size", + "name": "tree_size(tree::CTree{Float64})", + "arguments": [ + { "name": "tree", "type": 6 } + ], + "returns": { "type": 10 } + }, + { + "symbol": "countsame", + "name": "countsame(list::Ptr{MyTwoVec}, n::Int32)", + "arguments": [ + { "name": "list", "type": 11 }, + { "name": "n", "type": 4 } + ], + "returns": { "type": 4 } + } + ], + "types": [ + { + "id": 1, + "kind": "struct", + "name": "CVectorPair{Float32}", + "size": 32, + "alignment": 8, + "fields": [ + { "name": "from", "type": 3, "offset": 0 }, + { "name": "to", "type": 3, "offset": 16 } + ] + }, + { + "id": 2, + "kind": "primitive", + "name": "Float32", + "signed": false, + "bits": 32, + "size": 4, + "alignment": 4 + }, + { + "id": 3, + "kind": "struct", + "name": "CVector{Float32}", + "size": 16, + "alignment": 8, + "fields": [ + { "name": "length", "type": 4, "offset": 0 }, + { "name": "data", "type": 5, "offset": 8 } + ] + }, + { + "id": 4, + "kind": "primitive", + "name": "Int32", + "signed": true, + "bits": 32, + "size": 4, + "alignment": 4 + }, + { + "id": 5, + "kind": "pointer", + "name": "Ptr{Float32}", + "pointee": 2 + }, + { + "id": 6, + "kind": "struct", + "name": "CTree{Float64}", + "size": 16, + "alignment": 8, + "fields": [ + { "name": "children", "type": 8, "offset": 0 } + ] + }, + { + "id": 7, + "kind": "primitive", + "name": "Float64", + "signed": false, + "bits": 64, + "size": 8, + "alignment": 8 + }, + { + "id": 8, + "kind": "struct", + "name": "CVector{CTree{Float64}}", + "size": 16, + "alignment": 8, + "fields": [ + { "name": "length", "type": 4, "offset": 0 }, + { "name": "data", "type": 9, "offset": 8 } + ] + }, + { + "id": 9, + "kind": "pointer", + "name": "Ptr{CTree{Float64}}", + "pointee": 6 + }, + { + "id": 10, + "kind": "primitive", + "name": "Int64", + "signed": true, + "bits": 64, + "size": 8, + "alignment": 8 + }, + { + "id": 11, + "kind": "pointer", + "name": "Ptr{MyTwoVec}", + "pointee": 12 + }, + { + "id": 12, + "kind": "struct", + "name": "MyTwoVec", + "size": 8, + "alignment": 4, + "fields": [ + { "name": "x", "type": 4, "offset": 0 }, + { "name": "y", "type": 4, "offset": 4 } + ] + } + ] +} diff --git a/test/bindinginfo_libsimple.log b/test/bindinginfo_libsimple.log deleted file mode 100644 index be5c57a..0000000 --- a/test/bindinginfo_libsimple.log +++ /dev/null @@ -1,15 +0,0 @@ -countsame(list::Ptr{MyTwoVec}, n::Int32)::Int32 -copyto_and_sum(fromto::CVectorPair{Float32})::Float32 - -MyTwoVec - x::Int32[0] - y::Int32[4] -8 bytes -CVectorPair{Float32} - from::CVector{Float32}[0] - to::CVector{Float32}[16] -32 bytes -CVector{Float32} - length::Int32[0] - data::Ptr{Float32}[8] -16 bytes diff --git a/test/runtests.jl b/test/runtests.jl index ee96233..692a0e5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,10 @@ using JuliaLibWrapping +using OrderedCollections using Test +import JuliaLibWrapping: StructDesc, FieldDesc, PointerDesc, PrimitiveTypeDesc, TypeDesc +import JuliaLibWrapping: sort_declarations! + function onlymatch(f, collection) matches = filter(f, collection) if length(matches) != 1 @@ -10,76 +14,156 @@ function onlymatch(f, collection) end @testset "JuliaLibWrapping.jl" begin - @testset "parselog" begin - entrypoints, typedescs = parselog("bindinginfo_libsimple.log") + @testset "import_abi_info" begin + abi_info = import_abi_info("bindinginfo_libsimple.json") + (; entrypoints, typeinfo) = abi_info - methdesc = onlymatch(md -> md.name == "copyto_and_sum", entrypoints) - @test methdesc.return_type == "Float32" + methdesc = onlymatch(md -> md.symbol == "copyto_and_sum", entrypoints) + @test typeinfo[methdesc.return_type].name == "Float32" @test length(methdesc.args) == 1 argdesc = only(methdesc.args) @test argdesc.name == "fromto" - @test argdesc.type == "CVectorPair{Float32}" + @test typeinfo[argdesc.type].name == "CVectorPair{Float32}" @test argdesc.isva == false - @test sprint(show, methdesc) == "copyto_and_sum(fromto::CVectorPair{Float32})::Float32" - methdesc = onlymatch(md -> md.name == "countsame", entrypoints) - @test methdesc.return_type == "Int32" + methdesc = onlymatch(md -> md.symbol == "countsame", entrypoints) + @test typeinfo[methdesc.return_type].name == "Int32" @test length(methdesc.args) == 2 argdesc1, argdesc2 = methdesc.args @test argdesc1.name == "list" - @test argdesc1.type == "Ptr{MyTwoVec}" + @test typeinfo[argdesc1.type].name == "Ptr{MyTwoVec}" @test argdesc1.isva == false @test argdesc2.name == "n" - @test argdesc2.type == "Int32" + @test typeinfo[argdesc2.type].name == "Int32" @test argdesc2.isva == false - @test length(typedescs) == 3 - tdesc = typedescs["CVectorPair{Float32}"] + @test length(typeinfo) >= 3 + findtype(descs, name) = (k = collect(keys(descs)); k[findfirst((id)->descs[id].name === name, k)]) + + tdesc = typeinfo[findtype(typeinfo, "CVectorPair{Float32}")] @test tdesc.name == "CVectorPair{Float32}" @test length(tdesc.fields) == 2 @test tdesc.fields[1].name == "from" - @test tdesc.fields[1].type == "CVector{Float32}" + @test typeinfo[tdesc.fields[1].type].name == "CVector{Float32}" @test tdesc.fields[1].offset == 0 @test tdesc.fields[2].name == "to" - @test tdesc.fields[2].type == "CVector{Float32}" + @test typeinfo[tdesc.fields[2].type].name == "CVector{Float32}" @test tdesc.fields[2].offset == 16 @test tdesc.size == 32 - tdesc = typedescs["CVector{Float32}"] + tdesc = typeinfo[findtype(typeinfo, "CVector{Float32}")] @test tdesc.name == "CVector{Float32}" @test length(tdesc.fields) == 2 @test tdesc.fields[1].name == "length" - @test tdesc.fields[1].type == "Int32" + @test typeinfo[tdesc.fields[1].type].name == "Int32" @test tdesc.fields[1].offset == 0 @test tdesc.fields[2].name == "data" - @test tdesc.fields[2].type == "Ptr{Float32}" + @test typeinfo[tdesc.fields[2].type].name == "Ptr{Float32}" @test tdesc.fields[2].offset == 8 @test tdesc.size == 16 - @test haskey(typedescs, "CVectorPair{Float32}") - @test haskey(typedescs, "CVector{Float32}") - tdesc = typedescs["MyTwoVec"] + tdesc = typeinfo[findtype(typeinfo, "MyTwoVec")] @test tdesc.name == "MyTwoVec" @test length(tdesc.fields) == 2 @test tdesc.fields[1].name == "x" - @test tdesc.fields[1].type == "Int32" + @test typeinfo[tdesc.fields[1].type].name == "Int32" @test tdesc.fields[1].offset == 0 @test tdesc.fields[2].name == "y" - @test tdesc.fields[2].type == "Int32" + @test typeinfo[tdesc.fields[2].type].name == "Int32" @test tdesc.fields[2].offset == 4 @test tdesc.size == 8 - name2idx = Dict(name => i for (i, name) in enumerate(keys(typedescs))) + name2idx = Dict(desc.name => i for (i, desc) in enumerate(values(typeinfo))) @test name2idx["CVectorPair{Float32}"] > name2idx["CVector{Float32}"] + end + + @testset "sort_declarations!" begin + unsorted = OrderedDict{Int, TypeDesc}( + 1 => StructDesc( + "test_struct1", + 0, # size + 0, # alignment + FieldDesc[ + FieldDesc("field1", #= type =# 2, #= offset =# 0), + FieldDesc("field2", #= type =# 3, #= offset =# 0), + ], + ), + 2 => StructDesc( + "test_struct2", + 0, # size + 0, # alignment + FieldDesc[ + FieldDesc("field1", #= type =# 3, #= offset =# 0), + FieldDesc("field2", #= type =# 3, #= offset =# 0), + ], + ), + 3 => PrimitiveTypeDesc("UInt16"), + ) + sorted = copy(unsorted) + fwd_decls = sort_declarations!(sorted) + + # No recursive types, so this should require no forward declarations + @test isempty(fwd_decls) + # There is only one order that these types could be defined such that + # dependencies are defined before they are used. + @test collect(keys(sorted)) == Int[3,2,1] + + unsorted = OrderedDict{Int, TypeDesc}( + 1 => StructDesc( + "test_struct1", + 0, # size + 0, # alignment + FieldDesc[ + FieldDesc("field1", #= type =# 2, #= offset =# 0), + FieldDesc("field2", #= type =# 5, #= offset =# 0), + ], + ), + 2 => PointerDesc("pointer1", #= pointee_type =# 3), + 3 => StructDesc( + "test_struct2", + 0, # size + 0, # alignment + FieldDesc[ + FieldDesc("field1", #= type =# 5, #= offset =# 0), + FieldDesc("field2", #= type =# 5, #= offset =# 0), + ], + ), + 4 => PointerDesc("pointer2", #= pointee_type =# 1), + 5 => PrimitiveTypeDesc("UInt16"), + ) + sorted = copy(unsorted) + fwd_decls = sort_declarations!(sorted) - str = sprint(show, typedescs) - @test occursin("CVectorPair{Float32}(from::CVector{Float32}[0], to::CVector{Float32}[16]) (32 bytes)", str) - @test occursin("CVector{Float32}(length::Int32[0], data::Ptr{Float32}[8]) (16 bytes)", str) + # We added a pointer indirection, but it's non-recursive so we should + # require no forward declarations + @test isempty(fwd_decls) + + # Once again, there is only one order that we can emit these definitions + @test collect(keys(sorted)) == Int[5,3,2,1,4] + + # Modify the type definitions so that test_struct1 and test_struct2 are + # mutually recursive. + unsorted[3].fields[1] = FieldDesc("field1", #= type =# 4, #= offset =# 0) + + sorted = copy(unsorted) + fwd_decls = sort_declarations!(sorted) + + # At least one of the struct types should need to be forward-declared + @test !isempty(fwd_decls) + if fwd_decls == BitSet([1]) + # If 1 was forward-declared then 3 (and pointer to 1) is defined first + @test collect(keys(sorted)) == Int[5,4,3,2,1] + elseif fwd_decls == BitSet([3]) + # If 3 was forward-declared then 1 (and pointer to 3) is defined first + @test collect(keys(sorted)) == Int[5,2,1,4,3] + else + @test false # unexpected forward declarations + end end @testset "C wrapper" begin mktempdir() do path mkpath(path) dest = CProject(path, "libsimple") - entrypoints, typedescs = parselog("bindinginfo_libsimple.log") - wrapper(dest, entrypoints, typedescs) + abi_info = import_abi_info("bindinginfo_libsimple.json") + wrapper(dest, abi_info) headerfile = joinpath(dest.dir, dest.headerbase * ".h") @test isfile(headerfile) @@ -89,12 +173,12 @@ end @test occursin("#include ", content) @test occursin("#include ", content) @test occursin("#include ", content) - @test occursin("typedef struct {", content) + @test occursin("typedef struct CVector_Float32 {", content) @test occursin(" int32_t length;", content) @test occursin(" float* data;", content) - @test occursin("CVector_float_ from;", content) - @test occursin("CVector_float_ to;", content) - @test occursin("float copyto_and_sum(CVectorPair_float_ fromto);", content) + @test occursin("CVector_Float32 from;", content) + @test occursin("CVector_Float32 to;", content) + @test occursin("float copyto_and_sum(CVectorPair_Float32 fromto);", content) @test occursin("int32_t countsame(MyTwoVec* list, int32_t n);", content) end end