Skip to content

Adjust to new JSON ABI format #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ authors = ["Tim Holy <[email protected]> and contributors"]

[deps]
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON3/StructTypes might automate some of the json->julia parsing, but I am not sure there would be any real gain and this seems fine to me.

OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"

[compat]
Graphs = "1"
JSON = "0.21.4"
OrderedCollections = "1.8.1"
julia = "1.12"

Expand Down
6 changes: 3 additions & 3 deletions src/JuliaLibWrapping.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ module JuliaLibWrapping
using OrderedCollections
using Graphs

export parselog, wrapper
export CProject
export import_abi_info, wrapper
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an improvement in naming, thanks

export CProject, ABIInfo

include("parselog.jl")
include("abi_import.jl")
include("c.jl")

end
209 changes: 209 additions & 0 deletions src/abi_import.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
using JSON

struct FieldDesc
name::String
type::Int
offset::Int
end

struct PrimitiveTypeDesc
# TODO: support custom primitive types?
name::String
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to include bits?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to make pointer_filter the first positional argument so that one could use a do block? https://docs.julialang.org/en/v1/manual/style-guide/#Write-functions-with-argument-ordering-similar-to-Julia-Base

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
full_type_graph = build_type_graph(typedescs; pointer_filter = (id)->true)
full_type_graph = build_type_graph(typedescs; pointer_filter = Returns(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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this benefit from having a Base.show method? And should the comments be moved into a docstring?


"""
entrypoints, typedescs, forward_declarations = parselog(filename::String)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
entrypoints, typedescs, forward_declarations = parselog(filename::String)
abi_info = import_abi_info(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)
Loading
Loading