Skip to content

Commit c0e1c3e

Browse files
committed
Export ABI metadata in JSON format
With luck, this makes this information easier to consume downstream (and to expand on in the future). Also fixes a couple of obscure bugs that would cause mis-behavior in the presence of recursive types, or which could unintentionally mangle types in the event of sub-string collisions.
1 parent cb068c2 commit c0e1c3e

File tree

2 files changed

+201
-23
lines changed

2 files changed

+201
-23
lines changed

contrib/juliac/juliac-buildscript.jl

Lines changed: 187 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ const C_friendly_types = Base.IdSet{Any}([ # a few of these are redundant to
3535
])
3636

3737
function recursively_add_types!(types::Base.IdSet{DataType}, @nospecialize(T::DataType))
38-
while T <: Ptr
38+
T in types && return types
39+
while T.name === Ptr.body.name
40+
push!(types, T)
3941
T = T.parameters[1] # unwrap Ptr{...}
42+
T in types && return types
4043
end
4144
if T.name.module === Core && T C_friendly_types
4245
error("invalid type for juliac: ", T) # exclude internals (they may change)
@@ -50,37 +53,198 @@ function recursively_add_types!(types::Base.IdSet{DataType}, @nospecialize(T::Da
5053
return types
5154
end
5255

56+
struct TypeEmitter
57+
io::IO
58+
type_ids::IdDict{Any,Int}
59+
end
60+
61+
function escape_string_json(s::AbstractString)
62+
iob = IOBuffer()
63+
print(iob, '"')
64+
for c in s
65+
if c == '"'
66+
print(iob, "\\\"")
67+
elseif c == '\\'
68+
print(iob, "\\\\")
69+
elseif c == '\b'
70+
print(iob, "\\b")
71+
elseif c == '\f'
72+
print(iob, "\\f")
73+
elseif c == '\n'
74+
print(iob, "\\n")
75+
elseif c == '\r'
76+
print(iob, "\\r")
77+
elseif c == '\t'
78+
print(iob, "\\t")
79+
elseif '\x00' <= c <= '\x1f'
80+
print(iob, "\\u", lpad(string(UInt16(c), base=16), 4, '0'))
81+
else
82+
@assert isvalid(c) "invalid unicode character"
83+
print(iob, c)
84+
end
85+
end
86+
print(iob, '"')
87+
return String(take!(iob))
88+
end
89+
90+
function type_name_json(@nospecialize(dt::DataType))
91+
return escape_string_json(repr(dt))
92+
end
93+
94+
function field_name_json(@nospecialize(dt::DataType), field::Int)
95+
name = String(fieldname(dt, field))
96+
return escape_string_json(name)
97+
end
98+
99+
function emit_pointer_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
100+
pointee_type_id = ctx.type_ids[dt.parameters[1]]
101+
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
102+
indented_println("{")
103+
indented_println(" \"id\": ", ctx.type_ids[dt], ",")
104+
indented_println(" \"kind\": \"pointer\",")
105+
indented_println(" \"name\": ", type_name_json(dt), ",")
106+
indented_println(" \"pointee\": ", pointee_type_id)
107+
print(ctx.io, " " ^ indent, "}")
108+
end
109+
end
110+
111+
function emit_field_info!(ctx::TypeEmitter, @nospecialize(dt::DataType), field::Int; indent::Int = 0)
112+
desc = Base.DataTypeFieldDesc(dt)[field]
113+
type_id = ctx.type_ids[fieldtype(dt, field)]
114+
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
115+
indented_println("{")
116+
indented_println(" \"name\": ", field_name_json(dt, field), ",")
117+
indented_println(" \"offset\": ", desc.offset, ",")
118+
indented_println(" \"type\": ", type_id)
119+
print(ctx.io, " " ^ indent, "}")
120+
end
121+
end
122+
123+
function emit_struct_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
124+
type_id = ctx.type_ids[dt]
125+
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
126+
indented_println("{")
127+
indented_println(" \"id\": ", type_id, ",")
128+
indented_println(" \"kind\": \"struct\",")
129+
indented_println(" \"name\": ", type_name_json(dt), ",")
130+
indented_println(" \"size\": ", Core.sizeof(dt), ",")
131+
indented_println(" \"alignment\": ", Base.datatype_alignment(dt), ",")
132+
indented_println(" \"fields\": [")
133+
for i = 1:Base.datatype_nfields(dt)
134+
emit_field_info!(ctx, dt, i; indent = indent + 4)
135+
println(ctx.io, i == Base.datatype_nfields(dt) ? "" : ",")
136+
end
137+
indented_println(" ]")
138+
print(ctx.io, " " ^ indent, "}")
139+
end
140+
end
141+
142+
function emit_primitive_type!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
143+
type_id = ctx.type_ids[dt]
144+
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
145+
indented_println("{")
146+
indented_println(" \"id\": ", type_id, ",")
147+
indented_println(" \"kind\": \"primitive\",")
148+
indented_println(" \"name\": ", type_name_json(dt), ",")
149+
indented_println(" \"size\": ", Core.sizeof(dt), ",")
150+
indented_println(" \"alignment\": ", Base.datatype_alignment(dt))
151+
print(ctx.io, " " ^ indent, "}")
152+
end
153+
end
154+
155+
function emit_type_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
156+
if dt.name === Ptr.body.name
157+
emit_pointer_info!(ctx, dt; indent)
158+
elseif Base.isprimitivetype(dt)
159+
emit_primitive_type!(ctx, dt; indent)
160+
else
161+
emit_struct_info!(ctx, dt; indent)
162+
end
163+
end
164+
165+
function emit_method_info!(ctx::TypeEmitter, method::Core.Method; indent::Int = 0)
166+
(rt, sig) = method.ccallable
167+
(name, symbol) = let
168+
symbol = length(method.ccallable) > 2 ? Symbol(method.ccallable[3]) : method.name
169+
iob = IOBuffer()
170+
print(IOContext(iob, :print_method_signature_only => true), method)
171+
str = String(take!(iob))
172+
if symbol !== method.name && startswith(str, String(method.name))
173+
# Make a best-effort attempt to use the exported name
174+
#
175+
# Note: the `startswith` check is to make sure we support 'functor's in arg0,
176+
# which Base.@ccallable supports as long as they are singletons.
177+
str = replace(str, String(method.name) => String(symbol); count = 1)
178+
end
179+
(str, String(symbol))
180+
end
181+
182+
argnames = String.(Base.method_argnames(method))
183+
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
184+
indented_println("{")
185+
indented_println(" \"symbol\": ", escape_string_json(symbol), ",")
186+
indented_println(" \"name\": ", escape_string_json(name), ",")
187+
indented_println(" \"arguments\": [")
188+
for i in 2:length(sig.parameters)
189+
indented_println(" {")
190+
indented_println(" \"name\": ", escape_string_json(argnames[i]), ",")
191+
indented_println(" \"type\": ", ctx.type_ids[sig.parameters[i]])
192+
indented_println(" }", i == length(sig.parameters) ? "" : ",")
193+
end
194+
indented_println(" ],")
195+
indented_println(" \"returns\": {")
196+
indented_println(" \"type\": ", ctx.type_ids[rt])
197+
indented_println(" }")
198+
print(ctx.io, " " ^ indent, "}")
199+
end
200+
end
201+
202+
function emit_abi_info!(ctx::TypeEmitter, exported::Vector{Core.Method}, types::IdSet{DataType})
203+
println(ctx.io, "{")
204+
205+
# assign an ID to each type, so that we can refer to them
206+
for (i, T) in enumerate(types)
207+
ctx.type_ids[T] = i
208+
end
209+
210+
# print exported functions
211+
println(ctx.io, " \"functions\": [")
212+
for (i, method) in enumerate(exported)
213+
emit_method_info!(ctx, method; indent = 4)
214+
println(ctx.io, i == length(exported) ? "" : ",")
215+
end
216+
println(ctx.io, " ],")
217+
218+
# print type / structure information
219+
println(ctx.io, " \"types\": [")
220+
for (i, T) in enumerate(types)
221+
emit_type_info!(ctx, T; indent = 4)
222+
println(ctx.io, i == length(types) ? "" : ",")
223+
end
224+
println(ctx.io, " ]")
225+
226+
println(ctx.io, "}")
227+
end
228+
53229
function write_logfile(io::IO)
54-
iotmp = IOBuffer()
55230
types = Base.IdSet{DataType}()
231+
232+
# discover all exported methods + any types they reference
233+
exported = Core.Method[]
56234
Base.visit(Core.GlobalMethods) do method
57235
if isdefined(method, :ccallable)
58-
rt, sig = method.ccallable
59-
name = length(method.ccallable) > 2 ? Symbol(method.ccallable[3]) : method.name
60-
print(IOContext(iotmp, :print_method_signature_only => true), method)
61-
methodstr = String(take!(iotmp))
62-
if name !== method.name
63-
methodstr = replace(methodstr, String(method.name) => String(name))
64-
end
65-
println(io, methodstr, "::", rt)
236+
push!(exported, method)
237+
(rt, sig) = method.ccallable
66238
for T in sig.parameters[2:end]
67239
recursively_add_types!(types, T)
68240
end
241+
recursively_add_types!(types, rt)
69242
end
70243
end
71-
println(io) # blank line separates methods from types
72-
for T in types
73-
println(io, T)
74-
dtfd = Base.DataTypeFieldDesc(T)
75-
local fd
76-
for i = 1:Base.datatype_nfields(T)
77-
fd = dtfd[i]
78-
fn = fieldname(T, i)
79-
ft = fieldtype(T, i)
80-
println(io, " ", fn, "::", ft, "[", fd.offset, "]")
81-
end
82-
println(io, fd.offset + fd.size, " bytes")
83-
end
244+
245+
# print the discovered ABI info
246+
ctx = TypeEmitter(io, IdDict{Any,Int}())
247+
emit_abi_info!(ctx, exported, types)
84248
end
85249

86250
# Load user code

test/trimming/libsimple.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ struct MyTwoVec
1616
y::Int32
1717
end
1818

19+
struct CTree{T}
20+
# test that recursive datatypes work as expected
21+
children::CVector{CTree{T}}
22+
end
23+
24+
Base.@ccallable "tree_size" function size(tree::CTree{Float64})::Int64
25+
children = unsafe_wrap(Array, tree.children.data, tree.children.length)
26+
# Return the size of this sub-tree
27+
return sum(Int64[
28+
size(child)
29+
for child in children
30+
]; init=1)
31+
end
32+
1933
Base.@ccallable "copyto_and_sum" function badname(fromto::CVectorPair{Float32})::Float32
2034
from, to = unsafe_wrap(Array, fromto.from.data, fromto.from.length), unsafe_wrap(Array, fromto.to.data, fromto.to.length)
2135
copyto!(to, from)

0 commit comments

Comments
 (0)