Skip to content
This repository was archived by the owner on Mar 17, 2021. It is now read-only.

Commit 9fa4258

Browse files
committed
Package rewrite for Julia 1.3+
1 parent cae5bd9 commit 9fa4258

19 files changed

+21503
-1130
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
*.jl.cov
22
*.jl.*.cov
33
*.jl.mem
4-
deps/deps.jl
4+
/deps/build.log
5+
/deps/deps.jl
6+
/deps/libclang.jl
57
.*

Project.toml

+5-5
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ authors = ["Keith Rutkowski <[email protected]>"]
44
version = "0.1.0"
55

66
[deps]
7-
Clang = "40e3b903-d033-50b4-a0cc-940c62c95e31"
8-
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
9-
Todo = "b28a226c-6cff-11e9-1336-699fd753ab00"
107
CBinding = "d43a6710-96b8-4a2d-833c-c424785e5374"
8+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
9+
LLVM_jll = "86de99a1-58d6-5da7-8064-bd56ce2e322c"
1110

1211
[compat]
13-
julia = "^1.1"
14-
CBinding = "^0.6"
12+
julia = "^1.3"
13+
CBinding = "^0.8"
14+
LLVM_jll = "6.0.1,8.0.1"

README.md

+55-97
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,85 @@
11
# CBindingGen.jl
22

33
Automatically generate Julia bindings to C API's!
4+
We are developing CBindingGen.jl and CBinding.jl to support the use of arbitrary local C libraries, such as those provided by your Linux distribution, from Julia.
5+
46

57
# Usage
68

7-
CBindingGen.jl builds upon some of the processing capabilities of Clang.jl.
8-
Our approach also utilizes the components provided by the CBinding.jl package to more precisely and more completely interface with C API's.
9+
CBindingGen.jl seeks to be a comprehensive and automatic C bindings generation framework.
10+
The bindings utilize the CBinding.jl capabilities to precisely interface Julia with C.
911

10-
This package is primarily used at package build time when you wish to generate bindings to a C library on the system or one installed at build time.
11-
There are several important details about the process to point out.
12+
This package should only be used at package build time when you wish to generate bindings to a particular C library on the system or one built and installed at build time.
13+
The generated bindings file can then be included from your package code.
14+
Bindings files created by this package should _not_ be committed with your package and they are _not_ meant for editing by lowly humans once generated.
15+
Se let's get started with an example!
1216

13-
First, a filtering function must be defined to provide a means to limit the amount of material interfaced with the bindings.
14-
Remember that C `#include` statements insert files into the AST (and then every file they include gets inserted as well), so it is important to have an effective and accurate filter function.
1517

16-
Second, the proper compile flags must be specified.
17-
If you expect a C user's code to rely on `pkg-config` to get what flags are needed to include and link against your library, then those flags must also be provided to the `ConverterContext`.
18+
## Generating Bindings
1819

19-
The last detail required is to accurately and completely specify the library (or libraries) you want the bindings to link to as well as the list of header files to use to start the wrapping process (NOTICE: bindings are generated according to the filter function, so this list of header files only ensures that they are seen by the parser).
20-
An example of the process for generating bindings is shown below.
20+
CBindingGen.jl relies on the artifacts distributed with `LLVM_jll` for providing a `libclang.so` library and header files for your system.
21+
The following code shows what is necessary to generate bindings to `libclang.so`, and something like it would normally be placed in your package's `deps/build.jl` file.
2122

22-
```jl
23+
```julia
2324
using CBindingGen
24-
using CBindingGen.Clang
2525

26-
const hdrs = [
27-
"hdr1.h",
28-
"hdr2.h",
29-
"hdr3.h",
30-
]
26+
incdir = joinpath(dirname(dirname(LLVM_jll.libclang_path)), "include")
27+
hdrs = map(hdr -> joinpath("clang-c", hdr), readdir(joinpath(incdir, "clang-c")))
3128

32-
ctx = ConverterContext(["libexample"]) do decl
33-
header = filename(decl)
34-
name = spelling(decl)
35-
36-
# ignore anything not in the library's headers, e.g. "LibExample/hdr1.h"
37-
any(hdr -> endswith(header, joinpath("LibExample", hdr)), hdrs) || return false
38-
39-
# ignore the particular functions listed below (usually because they are in a header but not exposed with the library)
40-
decl isa CLFunctionDecl && name in (
41-
"missing1",
42-
"missing2",
43-
) && return false
29+
cvts = convert_headers(hdrs, args = ["-I", incdir]) do cursor
30+
header = CodeLocation(cursor).file
31+
name = string(cursor)
4432

45-
# ignore functions, variables, and macros starting with double-underscore
46-
startswith(name, "__") && (decl isa CLFunctionDecl || decl isa CLVarDecl || decl isa CLMacroDefinition) && return false
33+
# only wrap the libclang headers
34+
startswith(header, "$(incdir)/") || return false
4735

4836
return true
4937
end
5038

51-
parse_headers!(ctx, hdrs, args = ["-std=gnu99", "-DUSE_DEF=1])
52-
generate(ctx, joinpath(dirname(@__DIR__), "gen"), "example")
39+
open("bindings.jl", "w+") do io
40+
generate(io, LLVM_jll.libclang_path => cvts)
41+
end
42+
5343
```
5444

55-
The call to `generate(...)` in the example above will create several files in the package's "gen" directory which are prefixed with "example".
56-
Suffixes on the files help to indicate when each file can be included into a package.
57-
In a use case where the bindings are generated at build-time and the package is precompiled, the following example represents a template for providing the bindings as a Julia package.
45+
The `convert_headers` function takes an array of header files and the command line arguments, `args`.
46+
Any include directories, compiler options, or preprocessor definitions would be provided in `args` in the same way they would be used in your `clang` command line.
47+
An important detail of `convert_headers` is the filter function provided, here provided with the `do` syntax.
48+
The filter function allows you fine-grained control over what is converted to Julia as the C AST is traversed.
49+
In our example, we filter out any C constructs not defined within the header files we are interested in.
5850

59-
```jl
60-
module ExampleBindings
61-
# bring in dependencies
62-
using DepPkg1, DepPkg2
63-
64-
# CBinding is required by the CBindingGen-generated files
65-
import CBinding
66-
67-
# define opaque types (currently needs to be explicitly done)
68-
CBinding.@cstruct MyOpaqueType
69-
70-
# use of fully qualified names is a must if the bindings define a symbol from Base!
71-
Base.include(Base.@__MODULE__, Base.joinpath(Base.dirname(Base.@__DIR__), "gen", "example-atdevelopop.jl"))
72-
Base.include(Base.@__MODULE__, Base.joinpath(Base.dirname(Base.@__DIR__), "gen", "example-atcompile.jl"))
73-
Base.include(Base.@__MODULE__, Base.joinpath(Base.dirname(Base.@__DIR__), "gen", "example-atcompile_typedefs.jl"))
74-
Base.include(Base.@__MODULE__, Base.joinpath(Base.dirname(Base.@__DIR__), "gen", "example-atcompile_bindings.jl"))
75-
76-
function __init__()
77-
Base.include(Base.@__MODULE__, Base.joinpath(Base.dirname(Base.@__DIR__), "gen", "example-atload.jl"))
78-
end
79-
end
80-
```
51+
We use `CodeLocation(cursor)` to get the `file`, `line`, and `col` for the start of the C expression, while `CodeRange(cursor)` can be used to get the `start` and `stop` locations of the expression.
52+
Additionally, `string(cursor)` will get the "spelling" of the expression if you wish to filter particular C symbols.
8153

82-
It is also possible, completely at package load-time, to generate bindings and load them dynamically.
54+
The result of `convert_headers` is an array of `Converted` objects.
55+
`Converted` objects contain the Julia expression strings, as `expr`, and `comments` for storing exportable symbols an their comments ported from C.
8356

84-
```jl
85-
module ExampleBindings
86-
# bring in dependencies
87-
using DepPkg1, DepPkg2
88-
89-
# CBinding is required by the CBindingGen-generated files
90-
import CBinding, CBindingGen
91-
92-
# define opaque types (currently needs to be explicitly done)
93-
CBinding.@cstruct MyOpaqueType
94-
95-
function __init__()
96-
hdrs = [
97-
"hdr1.h",
98-
"hdr2.h",
99-
"hdr3.h",
100-
]
57+
Finally, the `generate` function is used to write the converted expressions for one or more libraries into a bindings file.
58+
59+
60+
## Loading Bindings
61+
62+
In order to load the bindings file within your package, it is best to define a `baremodule` within your package module to encapsulate the bindings.
63+
The namespace within the `baremodule` will have only a very few symbols that could conflict with those from C.
64+
We rely on macro usage within Julia to carefully avoid introducing any more conflicting symbols.
65+
CBinding provides `@macros` to define macros for many of the C-types in Julia, and it also defines `@CBinding()` to allow for unrestrained, but less-concise, access to Julia symbols.
66+
67+
```julia
68+
module MyModule
69+
baremodule LibClang
70+
using CBinding: @macros
71+
@macros
10172

102-
ctx = CBindingGen.ConverterContext(["libexample"]) do decl
103-
header = CBindingGen.Clang.filename(decl)
104-
name = CBindingGen.Clang.spelling(decl)
105-
106-
# ignore anything not in the library's headers, e.g. "LibExample/hdr1.h"
107-
Base.any(hdr -> Base.endswith(header, Base.joinpath("LibExample", hdr)), hdrs) || return false
108-
109-
# ignore the particular functions listed below (usually because they are in a header but not exposed with the library)
110-
decl isa CBindingGen.Clang.CLFunctionDecl && name in (
111-
"missing1",
112-
"missing2",
113-
) && return false
114-
115-
# ignore functions, variables, and macros starting with double-underscore
116-
Base.startswith(name, "__") && (decl isa CBindingGen.Clang.CLFunctionDecl || decl isa CBindingGen.Clang.CLVarDecl || decl isa CBindingGen.Clang.CLMacroDefinition) && return false
117-
118-
return true
119-
end
73+
const size_t = @Csize_t
74+
@include("bindings-deps.jl"))
12075

121-
CBindingGen.Clang.parse_headers!(ctx, hdrs, args = ["-std=gnu99", "-DUSE_DEF=1])
122-
@eval $(CBindingGen.generate(ctx))
76+
@include(@CBinding().joinpath(@CBinding().dirname(@CBinding().@__DIR__), "deps", "bindings.jl"))
12377
end
78+
79+
# other module code, such as high-level Julian code wrapping the bindings...
12480
end
12581
```
12682

127-
Once the bindings are generated, your next step would be to provide some higher-level Julia constructs to wrap the bindings with.
83+
Next is a section defining dependencies of the bindings and should be composed of hand-written code or imported packages that export the required symbols.
84+
Finishing the bindings module is the inclusion of the bindings file generated at build-time.
85+

deps/build.jl

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import LLVM_jll
2+
3+
4+
lib = LLVM_jll.libclang_path
5+
vers = let
6+
v = nothing
7+
for x in readdir(joinpath(dirname(lib), "clang"))
8+
if !isnothing(match(r"^\d+\.\d+\.\d+$", x)) && isdir(joinpath(dirname(lib), "clang", x))
9+
v = x
10+
break
11+
end
12+
end
13+
v
14+
end
15+
isnothing(vers) && error("Failed to determine the version of libclang")
16+
17+
18+
open(joinpath(@__DIR__, "deps.jl"), "w+") do o
19+
println(o, "const LIBCLANG_PATH = \"$(lib)\"")
20+
println(o, "const LIBCLANG_VERSION = \"$(vers)\"")
21+
end
22+
23+
open(joinpath(@__DIR__, "libclang.jl"), "w+") do o
24+
open(joinpath(@__DIR__, "libclang-$(vers).jl")) do i
25+
for l in eachline(i)
26+
println(o, replace(l, "@cbindings \"{{ LIBCLANG_PATH }}\" begin" => "@cbindings \"$(lib)\" begin"))
27+
end
28+
end
29+
end

deps/generate.jl

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using CBindingGen
2+
3+
4+
lib = ARGS[1] # LLVM_jll.libclang_path
5+
vers = let
6+
v = nothing
7+
for x in readdir(joinpath(dirname(lib), "clang"))
8+
if !isnothing(match(r"^\d+\.\d+\.\d+$", x)) && isdir(joinpath(dirname(lib), "clang", x))
9+
v = x
10+
break
11+
end
12+
end
13+
v
14+
end
15+
isnothing(vers) && error("Failed to determine the version of libclang")
16+
17+
18+
incdir = joinpath(dirname(dirname(lib)), "include")
19+
hdrs = map(hdr -> joinpath("clang-c", hdr), readdir(joinpath(incdir, "clang-c")))
20+
21+
cvts = convert_headers(hdrs, args = ["-I", incdir]) do cursor
22+
header = CodeLocation(cursor).file
23+
name = string(cursor)
24+
25+
# only wrap the libclang headers
26+
startswith(header, "$(incdir)/") || return false
27+
28+
# ignore function that uses time_t since we don't know what time_t is yet
29+
name == "clang_getFileTime" && return false
30+
31+
return true
32+
end
33+
34+
35+
open(joinpath(@__DIR__, "libclang-$(vers).jl"), "w+") do io
36+
generate(io, "{{ LIBCLANG_PATH }}" => cvts)
37+
end

0 commit comments

Comments
 (0)