diff --git a/.gitignore b/.gitignore index c4df2542005d4..90530bc921aa1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ /test/results_*.json /test/results_*.dat /test/deps +/test/trimming/Manifest.toml *.expmap *.exe diff --git a/Makefile b/Makefile index 3e4fc1356b08d..7e71117146fc2 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ julia-deps: | $(DIRS) $(build_datarootdir)/julia/base $(build_datarootdir)/julia julia-stdlib: | $(DIRS) julia-deps @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/stdlib -julia-base: julia-deps $(build_sysconfdir)/julia/startup.jl $(build_man1dir)/julia.1 $(build_datarootdir)/julia/julia-config.jl $(build_datarootdir)/julia/juliac/juliac.jl $(build_datarootdir)/julia/juliac/juliac-buildscript.jl $(build_datarootdir)/julia/juliac/juliac-trim-base.jl $(build_datarootdir)/julia/juliac/juliac-trim-stdlib.jl $(build_datarootdir)/julia/juliac/Artifacts.toml +julia-base: julia-deps $(build_sysconfdir)/julia/startup.jl $(build_man1dir)/julia.1 $(build_datarootdir)/julia/julia-config.jl $(build_datarootdir)/julia/juliac/juliac.jl $(build_datarootdir)/julia/juliac/abi_export.jl $(build_datarootdir)/julia/juliac/juliac-buildscript.jl $(build_datarootdir)/julia/juliac/juliac-trim-base.jl $(build_datarootdir)/julia/juliac/juliac-trim-stdlib.jl $(build_datarootdir)/julia/juliac/Artifacts.toml @$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/base julia-libccalltest: julia-deps diff --git a/contrib/juliac/abi_export.jl b/contrib/juliac/abi_export.jl new file mode 100644 index 0000000000000..1d5003d99ac92 --- /dev/null +++ b/contrib/juliac/abi_export.jl @@ -0,0 +1,223 @@ +const C_friendly_types = Base.IdSet{Any}([ # a few of these are redundant to make it easier to maintain + Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, Bool, + Cvoid, Cint, Cshort, Clong, Cuint, Cushort, Culong, Cssize_t, Csize_t, + Cchar, Cwchar_t, Cstring, Cwstring, + RawFD, +]) + +function recursively_add_types!(types::Base.IdSet{DataType}, @nospecialize(T::DataType)) + T in types && return types + while T.name === Ptr.body.name + push!(types, T) + T = T.parameters[1] # unwrap Ptr{...} + T in types && return types + end + if T.name.module === Core && T ∉ C_friendly_types + error("invalid type for juliac: ", T) # exclude internals (they may change) + end + push!(types, T) + for list in (T.parameters, fieldtypes(T)) + for S in list + recursively_add_types!(types, S) + end + end + return types +end + +struct TypeEmitter + io::IO + type_ids::IdDict{Any,Int} +end + +function escape_string_json(s::AbstractString) + iob = IOBuffer() + print(iob, '"') + for c in s + if c == '"' + print(iob, "\\\"") + elseif c == '\\' + print(iob, "\\\\") + elseif c == '\b' + print(iob, "\\b") + elseif c == '\f' + print(iob, "\\f") + elseif c == '\n' + print(iob, "\\n") + elseif c == '\r' + print(iob, "\\r") + elseif c == '\t' + print(iob, "\\t") + elseif '\x00' <= c <= '\x1f' + print(iob, "\\u", lpad(string(UInt16(c), base=16), 4, '0')) + else + @assert isvalid(c) "invalid unicode character" + print(iob, c) + end + end + print(iob, '"') + return String(take!(iob)) +end + +dequalify(str::AbstractString) = last(split(str, '.')) + +function type_name_json(@nospecialize(dt::DataType)) + return escape_string_json(dequalify(repr(dt))) +end + +function field_name_json(@nospecialize(dt::DataType), field::Int) + name = String(fieldname(dt, field)) + return escape_string_json(name) +end + +function emit_pointer_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0) + pointee_type_id = ctx.type_ids[dt.parameters[1]] + let indented_println(args...) = println(ctx.io, " " ^ indent, args...) + indented_println("{") + indented_println(" \"id\": ", ctx.type_ids[dt], ",") + indented_println(" \"kind\": \"pointer\",") + indented_println(" \"name\": ", type_name_json(dt), ",") + indented_println(" \"pointee_type_id\": ", pointee_type_id) + print(ctx.io, " " ^ indent, "}") + end +end + +function emit_field_info!(ctx::TypeEmitter, @nospecialize(dt::DataType), field::Int; indent::Int = 0) + desc = Base.DataTypeFieldDesc(dt)[field] + type_id = ctx.type_ids[fieldtype(dt, field)] + print(ctx.io, " " ^ indent) + print(ctx.io, "{") + print(ctx.io, " \"name\": ", field_name_json(dt, field), ",") + print(ctx.io, " \"type_id\": ", type_id, ",") + print(ctx.io, " \"offset\": ", desc.offset, ",") + print(ctx.io, " \"isptr\": ", desc.isptr, ",") + print(ctx.io, " \"isfieldatomic\": ", Base.isfieldatomic(dt, field)) + print(ctx.io, " }") +end + +function emit_struct_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0) + type_id = ctx.type_ids[dt] + let indented_println(args...) = println(ctx.io, " " ^ indent, args...) + indented_println("{") + indented_println(" \"id\": ", type_id, ",") + indented_println(ismutabletype(dt) ? " \"kind\": \"mutable struct\"," : " \"kind\": \"struct\",") + indented_println(" \"name\": ", type_name_json(dt), ",") + indented_println(" \"size\": ", Core.sizeof(dt), ",") + indented_println(" \"alignment\": ", Base.datatype_alignment(dt), ",") + indented_println(" \"fields\": [") + for i = 1:Base.datatype_nfields(dt) + emit_field_info!(ctx, dt, i; indent = indent + 4) + println(ctx.io, i == Base.datatype_nfields(dt) ? "" : ",") + end + indented_println(" ]") + print(ctx.io, " " ^ indent, "}") + end +end + +function emit_primitive_type!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0) + type_id = ctx.type_ids[dt] + let indented_println(args...) = println(ctx.io, " " ^ indent, args...) + indented_println("{") + indented_println(" \"id\": ", type_id, ",") + indented_println(" \"kind\": \"primitive\",") + indented_println(" \"name\": ", type_name_json(dt), ",") + indented_println(" \"signed\": ", (dt <: Signed), ",") + indented_println(" \"bits\": ", 8 * Base.packedsize(dt), ",") # size for reinterpret / in-register + indented_println(" \"size\": ", Base.aligned_sizeof(dt), ",") # size with padding / in-memory + indented_println(" \"alignment\": ", Base.datatype_alignment(dt)) + print(ctx.io, " " ^ indent, "}") + end +end + +function emit_type_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0) + if dt.name === Ptr.body.name + emit_pointer_info!(ctx, dt; indent) + elseif Base.isprimitivetype(dt) + emit_primitive_type!(ctx, dt; indent) + else + emit_struct_info!(ctx, dt; indent) + end +end + +function emit_method_info!(ctx::TypeEmitter, method::Core.Method; indent::Int = 0) + (rt, sig) = method.ccallable + (name, symbol) = let + symbol = length(method.ccallable) > 2 ? Symbol(method.ccallable[3]) : method.name + iob = IOBuffer() + print(IOContext(iob, :print_method_signature_only => true), method) + str = String(take!(iob)) + if symbol !== method.name && startswith(str, String(method.name)) + # Make a best-effort attempt to use the exported name + # + # Note: the `startswith` check is to make sure we support 'functor's in arg0, + # which Base.@ccallable supports as long as they are singletons. + str = replace(str, String(method.name) => String(symbol); count = 1) + end + (str, String(symbol)) + end + + argnames = String.(Base.method_argnames(method)) + let indented_println(args...) = println(ctx.io, " " ^ indent, args...) + indented_println("{") + indented_println(" \"symbol\": ", escape_string_json(symbol), ",") + indented_println(" \"name\": ", escape_string_json(name), ",") + indented_println(" \"arguments\": [") + for i in 2:length(sig.parameters) + print(ctx.io, " " ^ (indent + 4)) + print(ctx.io, "{") + print(ctx.io, " \"name\": ", escape_string_json(argnames[i]), ",") + print(ctx.io, " \"type_id\": ", ctx.type_ids[sig.parameters[i]]) + println(ctx.io, i == length(sig.parameters) ? " }" : " },") + end + indented_println(" ],") + indented_println(" \"returns\": { \"type_id\": ", ctx.type_ids[rt], " }") + print(ctx.io, " " ^ indent, "}") + end +end + +function emit_abi_info!(ctx::TypeEmitter, exported::Vector{Core.Method}, types::IdSet{DataType}) + println(ctx.io, "{") + + # assign an ID to each type, so that we can refer to them + for (i, T) in enumerate(types) + ctx.type_ids[T] = i + end + + # print exported functions + println(ctx.io, " \"functions\": [") + for (i, method) in enumerate(exported) + emit_method_info!(ctx, method; indent = 4) + println(ctx.io, i == length(exported) ? "" : ",") + end + println(ctx.io, " ],") + + # print type / structure information + println(ctx.io, " \"types\": [") + for (i, T) in enumerate(types) + emit_type_info!(ctx, T; indent = 4) + println(ctx.io, i == length(types) ? "" : ",") + end + println(ctx.io, " ]") + + println(ctx.io, "}") +end + +function write_abi_metadata(io::IO) + types = Base.IdSet{DataType}() + + # discover all exported methods + any types they reference + exported = Core.Method[] + Base.visit(Core.methodtable) do method + if isdefined(method, :ccallable) + push!(exported, method) + (rt, sig) = method.ccallable + for T in sig.parameters[2:end] + recursively_add_types!(types, T) + end + recursively_add_types!(types, rt) + end + end + + # print the discovered ABI info + ctx = TypeEmitter(io, IdDict{Any,Int}()) + emit_abi_info!(ctx, exported, types) +end diff --git a/contrib/juliac/juliac-buildscript.jl b/contrib/juliac/juliac-buildscript.jl index 8483b29d82764..5db05d1d5223b 100644 --- a/contrib/juliac/juliac-buildscript.jl +++ b/contrib/juliac/juliac-buildscript.jl @@ -20,6 +20,8 @@ if Base.get_bool_env("JULIA_USE_FLISP_PARSER", false) === false Base.JuliaSyntax.enable_in_core!() end +include(joinpath(@__DIR__, "abi_export.jl")) + # Load user code import Base.Experimental.entrypoint @@ -74,6 +76,14 @@ let include_result = Base.include(Main, ARGS[1]) if ARGS[3] == "true" Base.Compiler.add_ccallable_entrypoints!() end + + # Export info about entrypoints and structs needed to create header files + if length(ARGS) >= 4 + abi_export = ARGS[4] + open(abi_export, "w") do io + write_abi_metadata(io) + end + end end # Run the verifier in the current world (before build-script modifications), diff --git a/contrib/juliac/juliac.jl b/contrib/juliac/juliac.jl index eb6277785c789..99d7e1c8b5c2c 100644 --- a/contrib/juliac/juliac.jl +++ b/contrib/juliac/juliac.jl @@ -13,6 +13,7 @@ julia_cmd = `$(Base.julia_cmd()) --startup-file=no --history-file=no` cpu_target = get(ENV, "JULIA_CPU_TARGET", nothing) julia_cmd_target = `$(Base.julia_cmd(;cpu_target)) --startup-file=no --history-file=no` output_type = nothing # exe, sharedlib, sysimage +abi_export_file = nothing outname = nothing file = nothing add_ccallables = false @@ -25,6 +26,7 @@ if help !== nothing """ Usage: julia juliac.jl [--output-exe | --output-lib | --output-sysimage] [options] --experimental --trim= Only output code statically determined to be reachable + --export-abi Emit type / function information for the ABI (in JSON format) --compile-ccallable Include all methods marked `@ccallable` in output --relative-rpath Configure the library / executable to lookup all required libraries in an adjacent "julia/" folder --verbose Request verbose output @@ -97,6 +99,10 @@ let i = 1 i == length(ARGS) && error("Output specifier requires an argument") global outname = ARGS[i+1] i += 1 + elseif arg == "--export-abi" + i == length(ARGS) && error("Output specifier requires an argument") + global abi_export_file = ARGS[i+1] + i += 1 elseif arg == "--compile-ccallable" global add_ccallables = true elseif arg == "--verbose" @@ -171,7 +177,11 @@ function compile_products(enable_trim::Bool) end # Compile the Julia code - cmd = addenv(`$julia_cmd_target $project --output-o $img_path --output-incremental=no $strip_args $julia_args $(joinpath(@__DIR__,"juliac-buildscript.jl")) $absfile $output_type $add_ccallables`, "OPENBLAS_NUM_THREADS" => 1, "JULIA_NUM_THREADS" => 1) + args = String[absfile, output_type, string(add_ccallables)] + if abi_export_file !== nothing + push!(args, abi_export_file) + end + cmd = addenv(`$julia_cmd_target $project --output-o $img_path --output-incremental=no $strip_args $julia_args $(joinpath(@__DIR__,"juliac-buildscript.jl")) $(args)`, "OPENBLAS_NUM_THREADS" => 1, "JULIA_NUM_THREADS" => 1) verbose && println("Running: $cmd") if !success(pipeline(cmd; stdout, stderr)) println(stderr, "\nFailed to compile $file") diff --git a/test/Makefile b/test/Makefile index 8b9fc139488f4..61946e650dee5 100644 --- a/test/Makefile +++ b/test/Makefile @@ -75,6 +75,7 @@ gcext: trimming: @$(MAKE) -C $(SRCDIR)/$@ check $(TRIMMING_ARGS) + @$(MAKE) -C $(SRCDIR)/$@ clean $(TRIMMING_ARGS) clangsa: @$(MAKE) -C $(SRCDIR)/$@ diff --git a/test/trimming/Makefile b/test/trimming/Makefile index 2f29292d10bb5..af5b3f530c5d8 100644 --- a/test/trimming/Makefile +++ b/test/trimming/Makefile @@ -33,7 +33,7 @@ JULIAC_BUILDSCRIPT := $(shell $(JULIA) -e 'print(joinpath(Sys.BINDIR, Base.DATAR #============================================================================= -release: $(BIN)/hello$(EXE) $(BIN)/trimmability$(EXE) $(BIN)/basic_jll$(EXE) +release: $(BIN)/hello$(EXE) $(BIN)/trimmability$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/capplication$(EXE) $(BIN)/hello-o.a: $(SRCDIR)/hello.jl $(JULIAC_BUILDSCRIPT) $(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --output-o $@ --output-incremental=no --strip-ir --strip-metadata --experimental --trim $(JULIAC_BUILDSCRIPT) $< --output-exe true @@ -45,6 +45,9 @@ $(BIN)/basic_jll-o.a: $(SRCDIR)/basic_jll.jl $(JULIAC_BUILDSCRIPT) $(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --project=$(SRCDIR) -e "using Pkg; Pkg.instantiate()" $(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --project=$(SRCDIR) --output-o $@ --output-incremental=no --strip-ir --strip-metadata --experimental --trim $(JULIAC_BUILDSCRIPT) $< --output-exe true +$(BIN)/libsimple-o.a: $(SRCDIR)/libsimple.jl $(JULIAC_BUILDSCRIPT) + $(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --output-o $@ --output-incremental=no --strip-ir --strip-metadata --experimental --trim $(JULIAC_BUILDSCRIPT) $< --output-lib true $(BIN)/bindinginfo_libsimple.json + $(BIN)/hello$(EXE): $(BIN)/hello-o.a $(CC) -o $@ $(WHOLE_ARCHIVE) $< $(NO_WHOLE_ARCHIVE) $(CPPFLAGS_ADD) $(CPPFLAGS) $(CFLAGS_ADD) $(CFLAGS) $(LDFLAGS_ADD) $(LDFLAGS) @@ -54,11 +57,14 @@ $(BIN)/trimmability$(EXE): $(BIN)/trimmability-o.a $(BIN)/basic_jll$(EXE): $(BIN)/basic_jll-o.a $(CC) -o $@ $(WHOLE_ARCHIVE) $< $(NO_WHOLE_ARCHIVE) $(CPPFLAGS_ADD) $(CPPFLAGS) $(CFLAGS_ADD) $(CFLAGS) $(LDFLAGS_ADD) $(LDFLAGS) -check: $(BIN)/hello$(EXE) $(BIN)/trimmability$(EXE) $(BIN)/basic_jll$(EXE) - $(JULIA) --depwarn=error $(SRCDIR)/trimming.jl $< +$(BIN)/capplication$(EXE): $(SRCDIR)/capplication.c $(SRCDIR)/libsimple.h $(BIN)/libsimple-o.a + $(CC) -I$(BIN) -I$(SRCDIR) -I$(JULIA_LIBDIR) -o $@ $< -Wl,--whole-archive $(BIN)/libsimple-o.a -Wl,--no-whole-archive $(LDFLAGS_ADD) $(LDFLAGS) $(CPPFLAGS_ADD) $(CPPFLAGS) $(CFLAGS_ADD) $(CFLAGS) + +check: $(BIN)/hello$(EXE) $(BIN)/trimmability$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/capplication$(EXE) + $(JULIA) --startup-file=no --history-file=no --depwarn=error $(SRCDIR)/trimming.jl $< clean: - -rm -f $(BIN)/hello$(EXE) $(BIN)/trimmability$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/hello-o.a $(BIN)/trimmability-o.a $(BIN)/basic_jll-o.a + -rm -f $(BIN)/hello$(EXE) $(BIN)/trimmability$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/hello-o.a $(BIN)/trimmability-o.a $(BIN)/basic_jll-o.a $(BIN)/libsimple-o.a $(BIN)/libsimple.$(SHLIB_EXT) $(BIN)/capplication$(EXE) $(BIN)/bindinginfo_libsimple.json .PHONY: release clean check diff --git a/test/trimming/capplication.c b/test/trimming/capplication.c new file mode 100644 index 0000000000000..390faa282fdab --- /dev/null +++ b/test/trimming/capplication.c @@ -0,0 +1,20 @@ +#include +#include "libsimple.h" + +int main() { + // Example usage of the functions defined in libsimple.h + CVectorPair_Float32 vecPair; + vecPair.from.length = 3; + vecPair.from.data = (float[]){1.0f, 2.0f, 3.0f}; + vecPair.to.length = 3; + vecPair.to.data = (float[]){4.0f, 5.0f, 6.0f}; + + float sum = copyto_and_sum(vecPair); + printf("Sum of copied values: %f\n", sum); + + MyTwoVec list[] = {{1, 2}, {5, 5}, {3, 4}}; + int32_t count = countsame(list, 3); + printf("Count of same vectors: %d\n", count); + + return 0; +} diff --git a/test/trimming/libsimple.h b/test/trimming/libsimple.h new file mode 100644 index 0000000000000..ddd745f8f8ecc --- /dev/null +++ b/test/trimming/libsimple.h @@ -0,0 +1,31 @@ +#ifndef JULIALIB_LIBSIMPLE_H +#define JULIALIB_LIBSIMPLE_H +#include +#include +#include + +struct CTree_Float64; +typedef struct CVector_CTree_Float64 { + int32_t length; + struct CTree_Float64* data; +} CVector_CTree_Float64; +typedef struct CTree_Float64 { + CVector_CTree_Float64 children; +} CTree_Float64; +typedef struct MyTwoVec { + int32_t x; + int32_t y; +} MyTwoVec; +typedef struct CVector_Float32 { + int32_t length; + float* data; +} CVector_Float32; +typedef struct CVectorPair_Float32 { + CVector_Float32 from; + CVector_Float32 to; +} CVectorPair_Float32; + +float copyto_and_sum(CVectorPair_Float32 fromto); +int64_t tree_size(CTree_Float64 tree); +int32_t countsame(MyTwoVec* list, int32_t n); +#endif // JULIALIB_LIBSIMPLE_H diff --git a/test/trimming/libsimple.jl b/test/trimming/libsimple.jl new file mode 100644 index 0000000000000..64126ff8c384e --- /dev/null +++ b/test/trimming/libsimple.jl @@ -0,0 +1,58 @@ +module SimpleLib +# Test the logging of entrypoints and types in a C-callable Julia library. + +struct CVector{T} + length::Cint + data::Ptr{T} +end + +struct CVectorPair{T} + from::CVector{T} + to::CVector{T} +end + +struct MyTwoVec + x::Int32 + y::Int32 +end + +struct CTree{T} + # test that recursive datatypes work as expected + children::CVector{CTree{T}} +end + +Base.@ccallable "tree_size" function size(tree::CTree{Float64})::Int64 + children = unsafe_wrap(Array, tree.children.data, tree.children.length) + # Return the size of this sub-tree + return sum(Int64[ + size(child) + for child in children + ]; init=1) +end + +Base.@ccallable "copyto_and_sum" function badname(fromto::CVectorPair{Float32})::Float32 + from, to = unsafe_wrap(Array, fromto.from.data, fromto.from.length), unsafe_wrap(Array, fromto.to.data, fromto.to.length) + copyto!(to, from) + return sum(to) +end + +Base.@ccallable function countsame(list::Ptr{MyTwoVec}, n::Int32)::Int32 + list = unsafe_wrap(Array, list, n) + count = 0 + for v in list + count += v.x == v.y + end + return count +end + +export countsame, copyto_and_sum + +# FIXME? varargs +# Base.@ccallable function printints(x::Cint...)::Nothing +# for i in 1:length(x) +# print(x[i], " ") +# end +# println() +# end + +end diff --git a/test/trimming/trimming.jl b/test/trimming/trimming.jl index 07e9d92871f19..d255764acbf34 100644 --- a/test/trimming/trimming.jl +++ b/test/trimming/trimming.jl @@ -1,4 +1,9 @@ +import Pkg + +Pkg.add(["JSON"]) + using Test +using JSON @test length(ARGS) == 1 bindir = dirname(ARGS[1]) @@ -18,4 +23,63 @@ let exe_suffix = splitext(Base.julia_exename())[2] @test lines[2] == lines[3] @test Base.VersionNumber(lines[2]) ≥ v"1.5.7" @test filesize(basic_jll_exe) < filesize(unsafe_string(Base.JLOptions().image_file))/10 + + # Test that the shared library can be used in a C application + capplication_exe = joinpath(bindir, "capplication" * exe_suffix) + lines = split(readchomp(`$capplication_exe`), "\n") + @test length(lines) == 2 + @test lines[1] == "Sum of copied values: 6.000000" + @test lines[2] == "Count of same vectors: 1" + + # Test that the logging of entrypoints and types works correctly + str = read(joinpath(bindir, "bindinginfo_libsimple.json"), String) + + # The log should parse as valid JSON + abi = JSON.Parser.parse(str) + + # `copyto_and_sum` should have been exported + @test any(Bool[func["symbol"] == "copyto_and_sum" for func in abi["functions"]]) + + # `CVector{Float32}` should have been exported with the correct info + @show abi["types"] + @test any(Bool[type["name"] == "CVector{Float32}" for type in abi["types"]]) + CVector_Float32 = abi["types"][findfirst(type["name"] == "CVector{Float32}" for type in abi["types"])] + @test length(CVector_Float32["fields"]) == 2 + @test CVector_Float32["fields"][1]["offset"] == 0 + @test CVector_Float32["fields"][2]["offset"] == 8 + @test abi["types"][CVector_Float32["fields"][1]["type_id"]]["name"] == "Int32" + @test abi["types"][CVector_Float32["fields"][2]["type_id"]]["name"] == "Ptr{Float32}" + @test CVector_Float32["size"] == 16 + + # `CVectorPair{Float32}` should have been exported with the correct info + @test any(Bool[type["name"] == "CVectorPair{Float32}" for type in abi["types"]]) + CVectorPair_Float32 = abi["types"][findfirst(type["name"] == "CVectorPair{Float32}" for type in abi["types"])] + @test length(CVectorPair_Float32["fields"]) == 2 + @test CVectorPair_Float32["fields"][1]["offset"] == 0 + @test CVectorPair_Float32["fields"][2]["offset"] == 16 + @test abi["types"][CVectorPair_Float32["fields"][1]["type_id"]]["name"] == "CVector{Float32}" + @test abi["types"][CVectorPair_Float32["fields"][2]["type_id"]]["name"] == "CVector{Float32}" + @test CVectorPair_Float32["size"] == 32 + + # `CTree{Float64}` should have been exported with the correct info + @test any(Bool[type["name"] == "CTree{Float64}" for type in abi["types"]]) + CTree_Float64_id = findfirst(type["name"] == "CTree{Float64}" for type in abi["types"]) + CTree_Float64 = abi["types"][CTree_Float64_id] + @test length(CTree_Float64["fields"]) == 1 + @test CTree_Float64["fields"][1]["offset"] == 0 + CVector_CTree_Float64 = abi["types"][CTree_Float64["fields"][1]["type_id"]] + @test CVector_CTree_Float64["name"] == "CVector{CTree{Float64}}" + @test CTree_Float64["size"] == sizeof(UInt) * 2 + + # `CVector{CTree{Float64}}` should have been exported with the correct info + @test length(CVector_CTree_Float64["fields"]) == 2 + @test CVector_CTree_Float64["fields"][1]["offset"] == 0 + @test CVector_CTree_Float64["fields"][2]["offset"] == sizeof(UInt) + @test abi["types"][CVector_CTree_Float64["fields"][1]["type_id"]]["name"] == "Int32" + @test abi["types"][CVector_CTree_Float64["fields"][2]["type_id"]]["name"] == "Ptr{CTree{Float64}}" + @test CVector_CTree_Float64["size"] == sizeof(UInt) * 2 + + # `Ptr{CTree{Float64}}` should refer (recursively) back to the original type id + Ptr_CTree_Float64 = abi["types"][CVector_CTree_Float64["fields"][2]["type_id"]] + @test Ptr_CTree_Float64["pointee_type_id"] == CTree_Float64_id end