Static Compilation using StaticCompiler on Windows

PackageCompiler builds the entire sysimage which is very large. StaticCompiler allows static compilation of Julia into small native biniaries. It uses StaticTools which helps avoiding GC allocations.
Now it seems possible to use StaticCompiler on Windows. We adapted StaticCompiler the following way: So far StaticCompiler uses GPUCompiler to produce a LLVM IR, then uses GPUCompiler again to generate an objective file, and finally use clang to link and to produce an executable. In the adaption to Windows clang generates directly an executable from the LLVM IR.

Here is the forked repo.

3 Likes

So far StaticCompiler did not work in Windows. With the adaption above it is possible to generate small executable binaries from Julia code in Windows as well. We hope to exchange with people who are interested in static compilation of Julia in Windows, and need tiny native binaries.

3 Likes

Does StaticCompile work on Windows now? I think it’s on par with Linux now (after recent changes), at least for I/O (and I can’t think of more things that need to be Windows specific). For 64-bit Windows and 32-bit “not planned”.

I’m not on Windows, just curious about this, so I can’t check, nor your repo:
I see your repo is 21 commits ahead, but 4 behind. So is it needed for some things? Do your changes need to go upstream?

Yes, StaticCompiler works on Windows now. I did a pull request, and the developers took some things from that, and integrated it into StaticCompiler. (Not sure what you mean with “go upstream”?)

Thomas thank u a lot for your great work!
But i am sitll can’t to compile “hello world” on Windows.
Can u guide, how to do that?

With respect, Anthony

StaticCompiler is adapted to work on Windows.
However, when using Windows it is still necessary
to install the clang compiler. StaticCompiler provides an artifact that contains a clang version,
however it does not seem to work on Windows.
You can find clang in
https://releases.llvm.org
or
https://github.com/mstorsjo/llvm-mingw/releases

It does also seem to matter, which release of clang you use. I use clang version 17.0.4.

1 Like

Now I have programmed with StaticCompiler for one year, and I’d like to share some experiences. My main conclusion is that a lot of Julia code can be compiled, perhaps more than one might think.

Static compilation imposes some limitations to the julia code. GC allocations are not allowed. Additionally, the code should be type-stable. You cannot use non-costant global variables. All functions must return only native types, or must be inlined.

StaticTools provides a lot of alternative data types and functions to ensure allocation-free Julia code. In the following are some examples.

Strings are implemented as MallocString or StaticString, and can coded using the string macros c"" and m"", respectively. Both types ensure 0-terminated strings, just like C strings. A StaticString object has a fixed length. For a representing a MallocString object, memory is being allocated. After declaring a MallocString, the user itself is responsible for freeing the allocated memory.

function f()
  a = c"anne"
  b = m"mike"
  free(b)
  0
end

Arrays are represented as MallocVector, MallocArray, MallocMatrix,
StackVector or StackArray. The functions mfill and sfill can be used to initialize a Malloc- or StackVector, respectively. You can also use the constructors iteself, and have an undefined initialization.

a = mfill(m"", 2)
a[1] = m"anne"
a[2] = m"mike"
b = MallocVector{MallocString}(undef, 2)
b[1] = m"paul"
b[2] = m"john"
c = mfill(m"", 2, 3)
c[1, 1] = m"a"
c[1, 2] = m"b"
# ...
d = MallocMatrix{MallocString}(undef, 2, 3)
d[1, 1] = m"a"
d[1, 2] = m"b"
# ...
free(a)
free(b)
free(c)
free(d)
e = sfill(0,2)
e[1] = 1
e[2] = 2
f = StackVector{MallocString}(undef, 2)
f[1] = m"a"
f[2] = m"b"
g = StackMatrix{MallocString}(undef, 2, 3)
g[1, 1] = m"a"
g[1, 2] = m"b"
# ...

A composite type of a StaticString, e.g. StackVector{StaticString}(undef, 2) does not to seem possible.

Tuples, NamedTuples and Structs can be used almost in the usual way.

function f()
  t = (2, 3.3, m"a")
  a, b, c = t
  printf(c"a: %d\n", a)
  printf(c"b: %g\n", b)
  printf(c"c: %s\n", c)

  n = (x = 1, y = 1)
  printf(c"n.x: %d\n", n.x)
  printf(c"n.y: %d\n", n.y)  

  d = NTuple{3, MallocString}((m"a",m"b",m"c"))
  for i in 1:3
    printf(c"%s\n", d[i])
  end

  0
end
struct A
  s :: MallocString
  n :: Int64
end

function h()
  a = A(m"a", 1)
  0
end

When using mutable structs and calling a function that assignes a MallocString, you have to inline the called function or the function call.

mutable struct A
  s :: MallocString
  n :: Int64
end

function f()
  a = A(m"a", 1)
  a.s = m"b"
  a.n = 2
  g(a)
end

@inline function g(a::A)
  a.s = m"c"
  a.n = 3
  b = m"a"
end

Dictionaries and Unions need heap allocation. Enabling static compilation for them is an open, promising project.

Printing to stdout can be done with print, println and printf (in C matter). Flushing (and a newline) does putchar.

a = m"a"
print(c"hello")
println(c"hello")
printf(c"a: %s\n", a)
putchar('\n')

Here are examples for reading and writing files. The function readline was adapted to Windows (I did a PR for StaticTools).

fp = fopen(file_name, m"r")
str = readline(fp)
fclose(fp)
fp = fopen(file_name, m"w")
printf(fp, c"hello")
fclose(fp)

StaticTools also supplies functions to run system functions, converting strings to numbers, time(), sleep (adapted for Windows), allocate memory, and load dll’s and apply functions of them.

StaticTools.system(c"dir")

float = StaticTools.strtod(c"12.34")[1]

int = StaticTools.strtol(c"12")[1]

t_now = StaticTools.time())

StaticTools also provides the symbolcall-macro for calling a symbol via LLVM, and can be used to call further C functions. For example, the system function and the time function mentioned above, would be done with

function sys(s)
  @symbolcall system(pointer(s)::Ptr{UInt8})::Int
end

function time() 
  @symbolcall time(C_NULL::Ptr{Nothing})::Int
end

If you want the time in milliseconds, you can use

function time_ms() :: Int64
  @symbolcall clock()::Int
end

StaticTools provides the function usleep. However, it does not seem to work in Windows. With help of symbolcall it can be adapted.

function sleep_stc(secs::Real)
  millisecs = round(Int, secs * 1000)
  @symbolcall Sleep(millisecs :: Int) :: Int
end

Debugging - some guide lines:

When there is an error during compiling, the following error messages are typical:
f() did not infer to a concrete type. Got Union{}, an error when linking (ld.lld: error: undefined symbol), or clang: error: unable to execute command: program not executable, or a huge error message regarding the LLVM IR.

From those error message it is hardly possible to distinguish, whether the error is due to some restriction of the static compilation, or simply due to an program error in the Julia code, much less to debug the source code.

Thus it is a good practice, to

  1. let run the Julia code without compiling.
    Here you can use your favourite debug and trace tools, e.g. @info and @show. Writing in file seems to disturb the printing on stdout. So you would have to comment out any writing to a file.

  2. check whether any GC allocations occur.

  3. You can do this, e.g. with help of the tool @time, or some tool like Allocations. If allocations occur, identify the code segments that cause allocations, and find allocation-free alternatives.

  4. compile using compile_executable
    When here an error message occurs, then locate the code part, leading to the compilation error. After that find altenatives for the error-causing lines of code.

In general, it is of course helpful to start compiling smaller pieces of code, and then getting more and more code to compile.

My conclusion is that StaticCompiler a a great tool to compile StaticCompiler small, stand-alone, executable binaries. The generated binaries are in the range usually between around 90 and 300 kB (e.g. for 1500 lines Julia code I got a binary of 210 KB). In contrast to StaticCompiler, PackageCompiler would produce a system image, taking hundreds of MB.

StaticTools provides a lot of allocation-free functions
and data structures. A few have to be adapted to the respective operating system. Dictionaries and unions cannot be compiled so far. It would be a promising project, to extend StaticTools, implementing allocation-free alternatives of these data structures.

6 Likes