Skip to content
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

Implement variadic function #315

Merged
merged 2 commits into from
Jul 25, 2021
Merged
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
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ makedocs(;
),
pages=[
"Introduction" => "index.md",
"Tutorial" => "tutorial.md",
"Generator Tutorial" => "generator.md",
"LibClang Tutorial" => "tutorial.md",
"API Reference" => "api.md",
],
)
Expand Down
32 changes: 32 additions & 0 deletions docs/src/generator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generator Tutorial

## Variadic Function
With the help of `@ccall` macro, variadic C functions can be from Julia, for example, `@ccall printf("%d\n"::Cstring; 123::Cint)::Cint` can be used to call the C function `printf`. Note that those arguments after the semicolon `;` are variadic arguments.

If `wrap_variadic_function` in `codegen` section of options is set to `true`, `Clang.jl` will generate wrappers for variadic C functions, for example, `printf` will be wrapped as:
```julia
@generated function printf(fmt, va_list...)
:(@ccall(libexample.printf(fmt::Ptr{Cchar}; $(to_c_type_pairs(va_list)...))::Cint))
end
```
It can be called just like normal Julia functions without specifying types: `LibExample.printf("%d\n", 123)`.

!!! note
Although variadic functions are supported, the C type `va_list` can not be used from Julia.

### Type Correspondence

However, variadic C functions must be called with the correct argument types. The most useful ones are listed below:

| C type | ccall signature | Julia type |
|-------------------------------------|--------------------------------------------------|----------------------------------------|
| Integers and floating point numbers | the same type | the same type |
| Struct `T` | a concrete Julia struct `T` with the same layout | `T` |
| Pointer (`T*`) | `Ref{T}` or `Ptr{T}` | `Ref{T}` or `Ptr{T}` or any array type |
| String (`char*`) | `Cstring` or `Ptr{Cchar}` | `String` |

As observed from the table, if you want to pass strings or arrays to C, you need to annotate the type as `Ptr{T}` or `Ref{T}` (or `Cstring`), otherwise the struct that represents the `String` or `Array`type instead of the buffer itself will be passed. There are two methods to pass arguments of these types:
* directly use @ccall macro: `@ccall printf("%s\n"; "hello"::Cstring)::Cint`. You can also create wrappers for common use cases of this.
* overload `to_c_type` to map Julia type to correct ccall signature type: add `to_c_type(::Type{String}) = Cstring` to prologue (prologue can be added by setting `prologue_file_path` in options). Then all arguments of String will be annotated as `Cstring`.

For a complete tutorial on calling C functions, refer to [Calling C and Fortran Code](https://docs.julialang.org/en/v1/manual/calling-c-and-fortran-code/#Calling-C-and-Fortran-Code) in the Julia manual.
2 changes: 1 addition & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ investment may not pay off.

The above-mentioned C-bindings generator only exposes several entry points for customization.
In fact, it's actually not that hard to directly build your own C-bindings generator,
for example, the following script is used for generating `LibClang`, you could refer to [Tutorial](@ref) for
for example, the following script is used for generating `LibClang`, you could refer to [LibClang Tutorial](@ref) for
further details.

Write a config file `generator.toml`:
Expand Down
2 changes: 1 addition & 1 deletion docs/src/tutorial.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Tutorial
# LibClang Tutorial
Clang is an open-source compiler built on the LLVM framework and targeting C, C++, and Objective-C (LLVM is also the JIT backend for Julia). Due to a highly modular design, Clang has in recent years become the core of a growing number of projects utilizing pieces of the compiler, such as tools for source-to-source translation, static analysis and security evaluation, and editor tools for code completion, formatting, etc.

While LLVM and Clang are written in C++, the Clang project maintains a C-exported interface called "libclang" which provides access to the abstract syntax tree and type representations. Thanks to the ubiquity of support for C calling conventions, a number of languages have utilized libclang as a basis for tooling related to C and C++.
Expand Down
3 changes: 3 additions & 0 deletions gen/generator.toml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ opaque_as_mutable_struct = true
# if true, use Julia 1.5's new `@ccall` macro
use_ccall_macro = true

# if true, variadic functions are wrapped with `@ccall` macro. Otherwise variadic functions are ignored.
wrap_variadic_function = false

# generate getproperty/setproperty! methods for the types in the following list
field_access_method_list = []

Expand Down
89 changes: 64 additions & 25 deletions src/generator/codegen.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,28 @@ function _get_func_name(cursor, options)
return library_name
end

function emit!(dag::ExprDAG, node::ExprNode{FunctionProto}, options::Dict; args...)
"Get argument names and types. Return (names, types)."
function _get_func_arg(cursor, options, dag)
# argument names
conflict_syms = get(options, "function_argument_conflict_symbols", [])

cursor = node.cursor

library_name = _get_func_name(cursor, options)
library_expr = Meta.parse(library_name)

func_name = Symbol(spelling(cursor))
func_args = get_function_args(cursor)
arg_types = [getArgType(getCursorType(cursor), i - 1) for i in 1:length(func_args)]
ret_type = translate(tojulia(getCursorResultType(cursor)), options)
# handle unnamed args
arg_names = Vector{Symbol}(undef, length(func_args))
for (i, arg) in enumerate(func_args)
ns = Symbol(name(arg))
safe_name = make_name_safe(ns)
safe_name = isempty(safe_name) ? "arg$i" : safe_name
# handle name collisions
if haskey(dag.tags, ns) || haskey(dag.ids, ns) || haskey(dag.ids_extra, ns)
safe_name *= "_"
elseif safe_name ∈ conflict_syms
safe_name = "_" * safe_name
end
arg_names[i] = Symbol(safe_name)
end

# argument types
arg_types = [getArgType(getCursorType(cursor), i - 1) for i in 1:length(func_args)]
args = Union{Expr,Symbol}[translate(tojulia(arg), options) for arg in arg_types]
for (i, arg) in enumerate(args)
# array function arguments should decay to pointers
Expand All @@ -50,21 +59,23 @@ function emit!(dag::ExprDAG, node::ExprNode{FunctionProto}, options::Dict; args.
end
end
end
return arg_names, args
end

# handle unnamed args
arg_names = Vector{Symbol}(undef, length(func_args))
for (i, arg) in enumerate(func_args)
ns = Symbol(name(arg))
safe_name = make_name_safe(ns)
safe_name = isempty(safe_name) ? "arg$i" : safe_name
# handle name collisions
if haskey(dag.tags, ns) || haskey(dag.ids, ns) || haskey(dag.ids_extra, ns)
safe_name *= "_"
elseif safe_name ∈ conflict_syms
safe_name = "_" * safe_name
end
arg_names[i] = Symbol(safe_name)
end
function _get_func_return_type(cursor, options)
translate(tojulia(getCursorResultType(cursor)), options)
end

function emit!(dag::ExprDAG, node::ExprNode{FunctionProto}, options::Dict; args...)
cursor = node.cursor

library_name = _get_func_name(cursor, options)
library_expr = Meta.parse(library_name)

func_name = Symbol(spelling(cursor))

arg_names, args = _get_func_arg(cursor, options, dag)
ret_type = _get_func_return_type(cursor, options)

is_strict_typed = get(options, "is_function_strictly_typed", false)
signature = if is_strict_typed
Expand Down Expand Up @@ -131,7 +142,35 @@ function emit!(dag::ExprDAG, node::ExprNode{FunctionNoProto}, options::Dict; arg
end

function emit!(dag::ExprDAG, node::ExprNode{FunctionVariadic}, options::Dict; args...)
# TODO: add impl
# @ccall is needed to support variadic argument
use_ccall_macro = get(options, "use_ccall_macro", true)
if use_ccall_macro
cursor = node.cursor
is_strict_typed = get(options, "is_function_strictly_typed", false)
arg_names, args = _get_func_arg(cursor, options, dag)
ret_type = _get_func_return_type(cursor, options)
library_name = _get_func_name(cursor, options)
library_expr = Meta.parse(library_name)
func_name = Symbol(spelling(cursor))

signature = is_strict_typed ? efunsig(func_name, arg_names, args) : Expr(:call, func_name, arg_names...)
push!(signature.args, :(va_list...))

fixed_args = map(arg_names, args) do name, type
:($name::$type)
end
va_list_expr = Expr(:$, :(to_c_type_pairs(va_list)...))
ccall_body = :($library_expr.$func_name($(fixed_args...); $va_list_expr)::$ret_type)
ccall_expr = Expr(:macrocall, Symbol("@ccall"), nothing, ccall_body)
body = quote
$(Meta.quot(ccall_expr))
end

generated = Expr(:function, signature, body)
ex = Expr(:macrocall, Symbol("@generated"), nothing, generated)
rm_line_num_node!(ex)
push!(node.exprs, ex)
end
return dag
end

Expand Down
12 changes: 12 additions & 0 deletions src/generator/passes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,7 @@ ProloguePrinter(file::AbstractString; info=true) = ProloguePrinter(file, info)

function (x::ProloguePrinter)(dag::ExprDAG, options::Dict)
general_options = get(options, "general", Dict())
codegen_options = get(options, "codegen", Dict())
log_options = get(general_options, "log", Dict())
show_info = get(log_options, "ProloguePrinter_log", x.show_info)
module_name = get(general_options, "module_name", "")
Expand All @@ -967,6 +968,7 @@ function (x::ProloguePrinter)(dag::ExprDAG, options::Dict)
prologue_file_path = get(general_options, "prologue_file_path", "")
use_native_enum = get(general_options, "use_julia_native_enum_type", false)
print_CEnum = get(general_options, "print_using_CEnum", true)
wrap_variadic_function = get(codegen_options, "wrap_variadic_function", false)

show_info && @info "[ProloguePrinter]: print to $(x.file)"
open(x.file, "w") do io
Expand Down Expand Up @@ -995,6 +997,16 @@ function (x::ProloguePrinter)(dag::ExprDAG, options::Dict)
println(io, "using CEnum")
println(io)
end

if wrap_variadic_function
println(io, """
to_c_type(t::Type) = t
to_c_type_pairs(va_list) = map(enumerate(to_c_type.(va_list))) do (ind, type)
:(va_list[\$ind]::\$type)
end
""")
end

# print prelogue patches
if !isempty(prologue_file_path)
println(io, read(prologue_file_path, String))
Expand Down
10 changes: 10 additions & 0 deletions src/generator/print.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ function pretty_print(io, node::ExprNode{FunctionProto}, options::Dict)
return nothing
end

function pretty_print(io, node::ExprNode{FunctionVariadic}, options::Dict)
isempty(node.exprs) && return
println(io, "# automatic type deduction for variadic arguments may not be what you want, please use with caution")
for expr in node.exprs
println(io, expr)
println(io)
end
return nothing
end

function pretty_print(io, node::ExprNode{FunctionNoProto}, options::Dict)
@assert !isempty(node.exprs)
file, line, col = get_file_line_column(node.cursor)
Expand Down