Skip to content

Commit

Permalink
add docstrings and load strings
Browse files Browse the repository at this point in the history
This code is mostly copy pasted from JuliaGeo/GDAL.jl#73.

Three transformations are applied:

- docstrings are added, created from PROJ Doxygen XML output
- functions that return a Cstring are wrapped in unsafe_string to return a String
- functions that return a Ptr{Cstring} are wrapped in unsafe_loadstringlist to return a Vector{String}
  • Loading branch information
visr committed Nov 19, 2019
1 parent 0b80cd1 commit 57db65a
Show file tree
Hide file tree
Showing 5 changed files with 1,274 additions and 14 deletions.
144 changes: 144 additions & 0 deletions gen/doc.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# this file is included by wrap.jl and provides several functions for building docstrings

"Build docstring for a function from a Doxygen XML node"
function build_function(node::EzXML.Node)
io = IOBuffer()

# code block with function definition
print(io, " ", text(node, "name"), "(")
nspace = position(io)
params = findall(node, "param")
if isempty(params)
print(io, ")")
end
for param in params
param !== first(params) && print(io, " "^nspace) # align
islast = param === last(params)
print(io, text(param, "type"))
# declname not always present (with void)
lastchar = islast ? ")" : ",\n"
if !isempty(findall("declname", param))
print(io, " ", text(param, "declname"), lastchar)
else
print(io, lastchar)
end
end
println(io, " -> ", text(node, "type"))

# brief description, always 1 present, sometimes only whitespace
desc = strip(nodecontent(findfirst("briefdescription", node)))
if !isempty(desc)
println(io, "\n", text(node, "briefdescription"))
end

# parameters
params = findall("detaileddescription/para/parameterlist/parameteritem", node)
if !isempty(params)
println(io, "\n### Parameters")
for param in params
print(io, "* **", text(param, "parameternamelist"), "**: ")
println(io, text(param, "parameterdescription"))
end
end

# returns
return_elems = findall("detaileddescription/para/simplesect[@kind='return']", node)
if !isempty(return_elems)
println(io, "\n### Returns")
println(io, text(return_elems[1], "para"))
end

String(take!(io))
end

"Build a one line docstring consisting of only the brief description from an XML node"
function brief_description(node::EzXML.Node)
# brief description, always 1 present, sometimes only whitespace
strip(nodecontent(findfirst("briefdescription", node)))
end

"Compose a Markdown docstring based on a Doxygen XML element"
function build_docstring(node::EzXML.Node)
kind = node["kind"]
if kind == "function"
build_function(node)
elseif kind in ("enum", "define", "typedef")
brief_description(node)
else
# "friend" and "variable" kinds remain
# but we leave them out, not needed
""
end
end

"Return the text of a subelement `el` of `node`"
function text(node::EzXML.Node, el::AbstractString)
s = findfirst(el, node)
s === nothing ? "" : strip(nodecontent(s))
end

"Wrap the one or multiline docstring in appropriate quotes"
function addquotes(docstr::AbstractString)
if '\n' in docstr
string("\"\"\"\n", docstr, "\"\"\"")
else
# one line docstring
repr(rstrip(docstr, '.'))
end
end

"Get the C name out of a expression"
function cname(ex)
if @capture(ex, function f_(args__) ccall((a_, b_), xs__) end)
String(eval(a))
else
# TODO make MacroTools.namify work for structs and macros
if MacroTools.isexpr(ex) && ex.head === :struct
String(ex.args[2])
elseif MacroTools.isexpr(ex) && ex.head === :macrocall
# if the enum has a type specified
String(ex.args[3].args[1])
else
String(namify(ex))
end
end
end

"Based on a name, find the best XML node to generate docs from"
function findnode(name::String, doc::EzXML.Document)
# Names are not unique. We know that kind='friend' (not sure what it is)
# does not give good docs and is never the only one, so we skip those.
# First we use XPath to find all nodes with this name and not kind='friend'.
memberdef = "/doxygen/compounddef/sectiondef/memberdef"
nofriend = "not(@kind='friend')" # :-(
nodes = findall("$memberdef[name='$name' and $nofriend]", doc)

if length(nodes) == 0
return nothing
elseif length(nodes) == 1
return first(nodes)
else
# If we get multiple nodes back, we have to select the best one.
# Looking at the documentation, sometimes there are two similar docstrings,
# but one comes from a .cpp file, as seen in location's file attribute,
# and the other comes from a .h file (.c is the third option).
# Even though this is a C binding, the .cpp node includes the argument names
# which makes for an easier to read docstring, since they can be referenced
# to the argument names in the parameters list.
# Therefore if .cpp is one of the options, go for that.

# ExXML uses libxml2 which only supports XPath 1.0, meaning
# ends-with(@file,'.cpp') is not available, but according to
# https://stackoverflow.com/a/11857166/2875964 we can rewrite this as
cpp = "'.cpp' = substring(@file, string-length(@file) - string-length('.cpp') +1)"

for node in nodes
cppnode = findfirst("location[$cpp]/..", node)
if cppnode !== nothing
return cppnode
end
end
# .cpp not present, just pick the first
return first(nodes)
end
end
91 changes: 88 additions & 3 deletions gen/wrap_proj.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,106 @@ So when updating the PROJBuilder provided version, also rerun this wrapper.
This way we ensure that the provided library has the same functions available
as the wrapped one. Furthermore this makes sure constants in `proj_common.jl`
like `PROJ_VERSION_PATCH`, which are just literals, are correct.
Several custom transformations are applied that should make using this package more convenient.
- docstrings are added, created from PROJ Doxygen XML output
- functions that return a Cstring are wrapped in unsafe_string to return a String
- functions that return a Ptr{Cstring} are wrapped in unsafe_loadstringlist to return a Vector{String}
These transformations are based on the code developed for GDAL.jl, see
https://github.com/JuliaGeo/GDAL.jl/blob/master/gen/README.md for more information
on how to construct the PROJ Doxygen XML file needed here.
=#

using Clang
using MacroTools
using EzXML

const xmlpath = joinpath(@__DIR__, "doxygen.xml")

# several functions for building docstrings
include(joinpath(@__DIR__, "doc.jl"))


"""
Custom rewriter for Clang.jl's C wrapper
Gets called with all expressions in a header file, or all expressiong in a common file.
If available, it adds docstrings before every expression, such that Clang.jl prints them
on top of the expression. The expressions themselves get sent to `rewriter(::Expr)`` for
further treatment.
"""
function rewriter(xs::Vector)
rewritten = Any[]
for x in xs
# Clang.jl inserts strings like "# Skipping MacroDefinition: X"
# keep these to get a sense of what we are missing
if x isa String
push!(rewritten, x)
continue
end
@assert x isa Expr

name = cname(x)
node = findnode(name, doc)
docstr = node === nothing ? "" : build_docstring(node)
isempty(docstr) || push!(rewritten, addquotes(docstr))
x2 = rewriter(x)
push!(rewritten, x2)
end
rewritten
end

"Rewrite expressions in the ways listed at the top of this file."
function rewriter(x::Expr)
if @capture(x,
function f_(fargs__)
ccall(fname_, rettype_, argtypes_, argvalues__)
end
)
# it is a function wrapper around a ccall

# bind the ccall such that we can easily wrap it
cc = :(ccall($fname, $rettype, $argtypes, $(argvalues...)))

cc′ = if rettype == :Cstring
:(unsafe_string($cc))
elseif rettype == :(Ptr{Cstring})
:(unsafe_loadstringlist($cc))
else
cc
end

# stitch the modified function expression back together
:(function $f($(fargs...))
$cc′
end) |> prettify
else
# do not modify expressions that are no ccall function wrappers
x
end
end

# parse GDAL's Doxygen XML file
const doc = readxml(xmlpath)

# should be here if you pkg> build Proj4
includedir = normpath(joinpath(@__DIR__, "..", "deps", "usr", "include"))
headerfiles = [joinpath(includedir, "proj.h")]

wc = init(; headers = headerfiles,
output_file = joinpath(@__DIR__, "proj_c.jl"),
common_file = joinpath(@__DIR__, "proj_common.jl"),
output_file = joinpath(@__DIR__, "..", "src", "proj_c.jl"),
common_file = joinpath(@__DIR__, "..", "src", "proj_common.jl"),
clang_includes = [includedir, CLANG_INCLUDE],
clang_args = ["-I", includedir],
header_wrapped = (root, current) -> root == current,
header_library = x -> "libproj",
clang_diagnostics = true,
)
rewriter = rewriter,
)

run(wc)

# delete Clang.jl helper files
rm(joinpath(@__DIR__, "..", "src", "LibTemplate.jl"))
rm(joinpath(@__DIR__, "..", "src", "ctypes.jl"))
19 changes: 19 additions & 0 deletions src/Proj4.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,23 @@ include("proj_functions.jl") # user-facing proj functions
"Get a global error string in human readable form"
error_message() = _strerrno()

"""
Load a null-terminated list of strings
It takes a `PROJ_STRING_LIST`, which is a `Ptr{Cstring}`, and returns a `Vector{String}`.
"""
function unsafe_loadstringlist(ptr::Ptr{Cstring})
strings = Vector{String}()
(ptr == C_NULL) && return strings
i = 1
cstring = unsafe_load(ptr, i)
while cstring != C_NULL
push!(strings, unsafe_string(cstring))
i += 1
cstring = unsafe_load(ptr, i)
end
strings
end


end # module
Loading

0 comments on commit 57db65a

Please sign in to comment.