Skip to content

Commit 567168a

Browse files
Add compile_executable (#56)
* Allow changeing filename base in generate_shlib, though leave obj as default * Add `generate_executable` * Add `compile_executable` * Update docstring for compile_executable * Add tests for compile_executable * llvmcall used in standalone test only works on Julia 1.8+ * Looks like clang_jll ain't gonna cut it for the standalone executables even on linux * Try annotating compiled function with Base.@ccallable * Let's try some inlining * Ah, so `@ccallable` requires a return type * Ok, so no `@ccallable` * Potential workaround on non-apple systems: use minimal wrapper * We can probably use clang_jll after all * Probably about time for a version bump * Add more minimal test for v1.7, fix specifier on advanced test * Implement changes requested in code review
1 parent d39a270 commit 567168a

2 files changed

Lines changed: 206 additions & 28 deletions

File tree

src/StaticCompiler.jl

Lines changed: 151 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ using Base: RefValue
77
using Serialization: serialize, deserialize
88
using Clang_jll: clang
99

10-
export compile, load_function
10+
export compile, load_function, compile_executable
1111
export native_code_llvm, native_code_typed, native_llvm_module
1212

1313
"""
1414
compile(f, types, path::String = tempname()) --> (compiled_f, path)
1515
16-
!!! Warning: this will fail on programs that heap allocate any memory, or have dynamic dispatch !!!
16+
!!! Warning: this will fail on programs that heap allocate any memory tracked by the GC, or have dynamic dispatch !!!
1717
1818
Statically compile the method of a function `f` specialized to arguments of the type given by `types`.
1919
2020
This will create a directory at the specified path (or in a temporary directory if you exclude that argument)
2121
that contains the files needed for your static compiled function. `compile` will return a
22-
`StaticCompiledFunction` object and `obj_path` which is the absolute path of the directory containing the
23-
compilation artifacts. The `StaticCompiledFunction` can be treated as if it is a function with a single
22+
`StaticCompiledFunction` object and `obj_path` which is the absolute path of the directory containing the
23+
compilation artifacts. The `StaticCompiledFunction` can be treated as if it is a function with a single
2424
method corresponding to the types you specified when it was compiled.
2525
2626
To deserialize and instantiate a previously compiled function, simply execute `load_function(path)`, which
@@ -68,14 +68,14 @@ path
6868
6969
0 directories, 3 files
7070
````
71-
* `obj.so` (or `.dylib` on MacOS) is a shared object file that can be linked to in order to execute your
72-
compiled julia function.
71+
* `obj.so` (or `.dylib` on MacOS) is a shared object file that can be linked to in order to execute your
72+
compiled julia function.
7373
* `obj.cjl` is a serialized `LazyStaticCompiledFunction` object which will be deserialized and instantiated
74-
with `load_function(path)`. `LazyStaticcompiledfunction`s contain the requisite information needed to link to the
75-
`obj.so` inside a julia session. Once it is instantiated in a julia session (i.e. by
76-
`instantiate(::LazyStaticCompiledFunction)`, this happens automatically in `load_function`), it will be of type
77-
`StaticCompiledFunction` and may be called with arguments of type `types` as if it were a function with a
78-
single method (the method determined by `types`).
74+
with `load_function(path)`. `LazyStaticcompiledfunction`s contain the requisite information needed to link to the
75+
`obj.so` inside a julia session. Once it is instantiated in a julia session (i.e. by
76+
`instantiate(::LazyStaticCompiledFunction)`, this happens automatically in `load_function`), it will be of type
77+
`StaticCompiledFunction` and may be called with arguments of type `types` as if it were a function with a
78+
single method (the method determined by `types`).
7979
"""
8080
function compile(f, _tt, path::String = tempname(); name = GPUCompiler.safe_name(repr(f)), kwargs...)
8181
tt = Base.to_tuple_type(_tt)
@@ -134,6 +134,81 @@ end
134134

135135
instantiate(f::StaticCompiledFunction) = f
136136

137+
138+
"""
139+
```julia
140+
compile_executable(f, types::Tuple, path::String, name::String=repr(f); filename::String=name, kwargs...)
141+
```
142+
Attempt to compile a standalone executable that runs function `f` with a type signature given by the tuple of `types`.
143+
144+
### Examples
145+
```julia
146+
julia> using StaticCompiler
147+
148+
julia> function puts(s::Ptr{UInt8}) # Can't use Base.println because it allocates.
149+
# Note, this `llvmcall` requires Julia 1.8+
150+
Base.llvmcall((\"""
151+
; External declaration of the puts function
152+
declare i32 @puts(i8* nocapture) nounwind
153+
154+
define i32 @main(i8*) {
155+
entry:
156+
%call = call i32 (i8*) @puts(i8* %0)
157+
ret i32 0
158+
}
159+
\""", "main"), Int32, Tuple{Ptr{UInt8}}, s)
160+
end
161+
puts (generic function with 1 method)
162+
163+
julia> function print_args(argc::Int, argv::Ptr{Ptr{UInt8}})
164+
for i=1:argc
165+
# Get pointer
166+
p = unsafe_load(argv, i)
167+
# Print string at pointer location (which fortunately already exists isn't tracked by the GC)
168+
puts(p)
169+
end
170+
return 0
171+
end
172+
173+
julia> compile_executable(print_args, (Int, Ptr{Ptr{UInt8}}))
174+
"/Users/foo/code/StaticCompiler.jl/print_args"
175+
176+
shell> ./print_args 1 2 3 4 Five
177+
./print_args
178+
1
179+
2
180+
3
181+
4
182+
Five
183+
```
184+
```julia
185+
julia> using StaticTools # So you don't have to define `puts` and friends every time
186+
187+
julia> hello() = println(c"Hello, world!") # c"..." makes a stack-allocated StaticString
188+
189+
julia> compile_executable(hello)
190+
"/Users/foo/code/StaticCompiler.jl/hello"
191+
192+
shell> ./hello
193+
Hello, world!
194+
```
195+
"""
196+
function compile_executable(f, _tt=(), path::String="./", name=GPUCompiler.safe_name(repr(f)); filename=name, kwargs...)
197+
tt = Base.to_tuple_type(_tt)
198+
tt == Tuple{} || tt == Tuple{Int, Ptr{Ptr{UInt8}}} || error("input type signature $_tt must be either () or (Int, Ptr{Ptr{UInt8}})")
199+
200+
rt = only(native_code_typed(f, tt))[2]
201+
isconcretetype(rt) || error("$f$_tt did not infer to a concrete type. Got $rt")
202+
203+
# Would be nice to use a compiler pass or something to check if there are any heap allocations or references to globals
204+
# Keep an eye on https://github.com/JuliaLang/julia/pull/43747 for this
205+
206+
generate_executable(f, tt, path, name, filename; kwargs...)
207+
208+
joinpath(abspath(path), filename)
209+
end
210+
211+
137212
module TestRuntime
138213
# dummy methods
139214
signal_exception() = return
@@ -160,7 +235,7 @@ end
160235

161236
"""
162237
```julia
163-
generate_shlib(f, tt, path::String, name::String; kwargs...)
238+
generate_shlib(f, tt, path::String, name::String, filenamebase::String="obj"; kwargs...)
164239
```
165240
Low level interface for compiling a shared object / dynamically loaded library
166241
(`.so` / `.dylib`) for function `f` given a tuple type `tt` characterizing
@@ -196,10 +271,10 @@ julia> ccall(StaticCompiler.generate_shlib_fptr(path, name), Float64, (Int64,),
196271
5.256496109495593
197272
```
198273
"""
199-
function generate_shlib(f, tt, path::String = tempname(), name = GPUCompiler.safe_name(repr(f)); kwargs...)
274+
function generate_shlib(f, tt, path::String = tempname(), name = GPUCompiler.safe_name(repr(f)), filenamebase::String="obj"; kwargs...)
200275
mkpath(path)
201-
obj_path = joinpath(path, "obj.o")
202-
lib_path = joinpath(path, "obj.$(Libdl.dlext)")
276+
obj_path = joinpath(path, "$filenamebase.o")
277+
lib_path = joinpath(path, "$filenamebase.$(Libdl.dlext)")
203278
open(obj_path, "w") do io
204279
job, kwargs = native_job(f, tt; name, kwargs...)
205280
obj, _ = GPUCompiler.codegen(:obj, job; strip=true, only_entry=false, validate=false)
@@ -216,9 +291,9 @@ function generate_shlib(f, tt, path::String = tempname(), name = GPUCompiler.saf
216291
end
217292

218293

219-
function generate_shlib_fptr(f, tt, path::String=tempname(), name = GPUCompiler.safe_name(repr(f)); temp::Bool=true, kwargs...)
294+
function generate_shlib_fptr(f, tt, path::String=tempname(), name = GPUCompiler.safe_name(repr(f)), filenamebase::String="obj"; temp::Bool=true, kwargs...)
220295
generate_shlib(f, tt, path, name; kwargs...)
221-
lib_path = joinpath(abspath(path), "obj.$(Libdl.dlext)")
296+
lib_path = joinpath(abspath(path), "$filenamebase.$(Libdl.dlext)")
222297
ptr = Libdl.dlopen(lib_path, Libdl.RTLD_LOCAL)
223298
fptr = Libdl.dlsym(ptr, "julia_$name")
224299
@assert fptr != C_NULL
@@ -236,7 +311,7 @@ Low level interface for obtaining a function pointer by `dlopen`ing a shared
236311
library given the `path` and `name` of a `.so`/`.dylib` already compiled by
237312
`generate_shlib`.
238313
239-
See also `StaticCompiler.enerate_shlib`.
314+
See also `StaticCompiler.generate_shlib`.
240315
241316
### Examples
242317
```julia
@@ -264,14 +339,70 @@ julia> test(100_000)
264339
5.256496109495593
265340
```
266341
"""
267-
function generate_shlib_fptr(path::String, name)
268-
lib_path = joinpath(abspath(path), "obj.$(Libdl.dlext)")
342+
function generate_shlib_fptr(path::String, name, filenamebase::String="obj")
343+
lib_path = joinpath(abspath(path), "$filenamebase.$(Libdl.dlext)")
269344
ptr = Libdl.dlopen(lib_path, Libdl.RTLD_LOCAL)
270345
fptr = Libdl.dlsym(ptr, "julia_$name")
271346
@assert fptr != C_NULL
272347
fptr
273348
end
274349

350+
"""
351+
```julia
352+
generate_executable(f, tt, path::String, name, filename=string(name); kwargs...)
353+
```
354+
Attempt to compile a standalone executable that runs `f`.
355+
356+
### Examples
357+
```julia
358+
julia> function test(n)
359+
r = 0.0
360+
for i=1:n
361+
r += log(sqrt(i))
362+
end
363+
return r/n
364+
end
365+
test (generic function with 1 method)
366+
367+
julia> path, name = StaticCompiler.generate_executable(test, Tuple{Int64}, "./scratch")
368+
```
369+
"""
370+
function generate_executable(f, tt, path::String = tempname(), name = GPUCompiler.safe_name(repr(f)), filename::String=string(name); kwargs...)
371+
mkpath(path)
372+
obj_path = joinpath(path, "$filename.o")
373+
exec_path = joinpath(path, filename)
374+
open(obj_path, "w") do io
375+
job, kwargs = native_job(f, tt; name, kwargs...)
376+
obj, _ = GPUCompiler.codegen(:obj, job; strip=true, only_entry=false, validate=false)
377+
378+
write(io, obj)
379+
flush(io)
380+
381+
# Pick a compiler
382+
cc = Sys.isapple() ? `cc` : clang()
383+
# Compile!
384+
if Sys.isapple()
385+
# Apple no longer uses _start, so we can just specify a custom entry
386+
entry = "_julia_$name"
387+
run(`$cc -e $entry $obj_path -o $exec_path`)
388+
else
389+
# Write a minimal wrapper to avoid having to specify a custom entry
390+
wrapper_path = joinpath(path, "wrapper.c")
391+
f = open(wrapper_path, "w")
392+
print(f, """int main(int argc, char** argv)
393+
{
394+
julia_$name(argc, argv);
395+
return 0;
396+
}""")
397+
close(f)
398+
run(`$cc $wrapper_path $obj_path -o $exec_path`)
399+
# Clean up
400+
run(`rm $wrapper_path`)
401+
end
402+
end
403+
path, name
404+
end
405+
275406
function native_code_llvm(@nospecialize(func), @nospecialize(types); kwargs...)
276407
job, kwargs = native_job(func, types; kwargs...)
277408
GPUCompiler.code_llvm(stdout, job; kwargs...)

test/runtests.jl

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ fib(n) = n <= 1 ? n : fib(n - 1) + fib(n - 2) # This needs to be defined globall
3333
# Trick to work around #40990
3434
_fib2(_fib2, n) = n <= 1 ? n : _fib2(_fib2, n-1) + _fib2(_fib2, n-2)
3535
fib2(n) = _fib2(_fib2, n)
36-
36+
3737
_, path = compile(fib2, (Int,))
3838
@test remote_load_call(path, 20) == fib(20)
39-
#@test compile(fib2, (Int,))[1](20) == fib(20)
39+
#@test compile(fib2, (Int,))[1](20) == fib(20)
4040
end
4141

4242
# Call binaries for testing
4343
# @testset "Generate binary" begin
4444
# fib(n) = n <= 1 ? n : fib(n - 1) + fib(n - 2)
45-
# libname = tempname()
45+
# libname = tempname()
4646
# generate_shlib(fib, (Int,), libname)
4747
# ptr = Libdl.dlopen(libname * "." * Libdl.dlext, Libdl.RTLD_LOCAL)
4848
# fptr = Libdl.dlsym(ptr, "julia_fib")
@@ -62,7 +62,7 @@ end
6262
end
6363
_, path = compile(sum_first_N_int, (Int,))
6464
@test remote_load_call(path, 10) == 55
65-
65+
6666
function sum_first_N_float64(N)
6767
s = Float64(0)
6868
for a in 1:N
@@ -116,10 +116,10 @@ end
116116

117117
# Julia wants to treat Tuple (and other things like it) as plain bits, but LLVM wants to treat it as something with a pointer.
118118
# We need to be careful to not send, nor receive an unwrapped Tuple to a compiled function.
119-
# The interface made in `compile` should handle this fine.
119+
# The interface made in `compile` should handle this fine.
120120
@testset "Send and receive Tuple" begin
121121
foo(u::Tuple) = 2 .* reverse(u) .- 1
122-
122+
123123
_, path = compile(foo, (NTuple{3, Int},))
124124
@test remote_load_call(path, (1, 2, 3)) == (5, 3, 1)
125125
end
@@ -172,7 +172,7 @@ end
172172
end
173173

174174
# This is a trick to get stack allocated arrays inside a function body (so long as they don't escape).
175-
# This lets us have intermediate, mutable stack allocated arrays inside our
175+
# This lets us have intermediate, mutable stack allocated arrays inside our
176176
@testset "Alloca" begin
177177
function f(N)
178178
# this can hold at most 100 Int values, if you use it for more, you'll segfault
@@ -186,7 +186,54 @@ end
186186
end
187187
_, path = compile(f, (Int,))
188188
@test remote_load_call(path, 20) == 20
189-
end
189+
end
190+
191+
@testset "Standalone Executables" begin
192+
# Minimal test with no `llvmcall`
193+
@inline function foo()
194+
v = 0.0
195+
n = 1000
196+
for i=1:n
197+
v += sqrt(n)
198+
end
199+
return 0
200+
end
201+
202+
filepath = compile_executable(foo, (), tempdir())
203+
204+
r = run(`$filepath`);
205+
@test isa(r, Base.Process)
206+
@test r.exitcode == 0
190207

208+
@static if VERSION>v"1.8.0-DEV" # The llvmcall here only works on 1.8+
209+
@inline function puts(s::Ptr{UInt8}) # Can't use Base.println because it allocates
210+
Base.llvmcall(("""
211+
; External declaration of the puts function
212+
declare i32 @puts(i8* nocapture) nounwind
191213
214+
define i32 @main(i8*) {
215+
entry:
216+
%call = call i32 (i8*) @puts(i8* %0)
217+
ret i32 0
218+
}
219+
""", "main"), Int32, Tuple{Ptr{UInt8}}, s)
220+
end
221+
222+
@inline function print_args(argc::Int, argv::Ptr{Ptr{UInt8}})
223+
for i=1:argc
224+
# Get pointer
225+
p = unsafe_load(argv, i)
226+
# Print string at pointer location (which fortunately already exists isn't tracked by the GC)
227+
puts(p)
228+
end
229+
return 0
230+
end
231+
232+
filepath = compile_executable(print_args, (Int, Ptr{Ptr{UInt8}}), tempdir())
233+
234+
r = run(`$filepath Hello, world!`);
235+
@test isa(r, Base.Process)
236+
@test r.exitcode == 0
237+
end
238+
end
192239
# data structures, dictionaries, tuples, named tuples

0 commit comments

Comments
 (0)