Skip to content
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ebb3ded
juliac: log entrypoints and nonstandard typedefs
timholy Jul 27, 2025
1099753
minor Makefile improvements
timholy Jul 27, 2025
e90786b
Nicer printing
timholy Jul 28, 2025
ce0f007
Use IdSet instead of Union
timholy Jul 29, 2025
334f613
Try using the library
timholy Jul 29, 2025
612350a
Remove Array from C-friendly types
timholy Jul 30, 2025
c2dffaf
Get the shared library working
timholy Jul 30, 2025
ff8931c
Clean up after trimming tests
timholy Jul 30, 2025
52878f3
Update to JuliaLibWrapping .h file
timholy Jul 30, 2025
106868a
Split `write_logfile` into separate function
timholy Jul 30, 2025
cb068c2
Update contrib/juliac/juliac-buildscript.jl
timholy Jul 31, 2025
c0e1c3e
Export ABI metadata in JSON format
topolarity Aug 1, 2025
a1b8349
Move ABI export functionality to `abi_export.jl`
topolarity Aug 1, 2025
0a744d6
Print field / argument info more compactly
topolarity Aug 1, 2025
e1d3052
Add `--export-abi` arg to `juliac.jl`
topolarity Aug 1, 2025
d51511a
Add extra metadata for `primitive` types
topolarity Aug 1, 2025
76057d4
Minor fix-up for makefile paths
topolarity Aug 4, 2025
f5ffa0c
Merge remote-tracking branch 'julialang/master' into teh/juliac_heade…
topolarity Aug 4, 2025
0c38e91
Update header file to latest from `JuliaLibWrapping.jl`
topolarity Aug 4, 2025
6fd68ec
Adjust to `Core.MethodTable` -> `Core.methodtable`
topolarity Aug 4, 2025
77b1916
Minor fix-up for makefile paths
topolarity Aug 4, 2025
c8a0c0b
Update `test/trimming/trimming.jl` for JSON ABI format
topolarity Aug 4, 2025
af8e042
Rename `type` to `type_id`
topolarity Aug 19, 2025
6161dab
Add recursive type tests
topolarity Aug 19, 2025
cb503ee
Update contrib/juliac/abi_export.jl
timholy Aug 31, 2025
93a0557
Add isptr and isfieldatomic
timholy Aug 31, 2025
d9ea0eb
Merge branch 'master' into teh/juliac_headerlogs
timholy Aug 31, 2025
be561b7
Fix spacing
timholy Aug 31, 2025
7adc37b
Debug tests
timholy Aug 31, 2025
53b84e6
Remove project
timholy Aug 31, 2025
4dfc45b
Remove module qualification
timholy Sep 1, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
/test/results_*.json
/test/results_*.dat
/test/deps
/test/trimming/Manifest.toml

*.expmap
*.exe
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ julia-deps: | $(DIRS) $(build_datarootdir)/julia/base $(build_datarootdir)/julia
julia-stdlib: | $(DIRS) julia-deps
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/stdlib

julia-base: julia-deps $(build_sysconfdir)/julia/startup.jl $(build_man1dir)/julia.1 $(build_datarootdir)/julia/julia-config.jl $(build_datarootdir)/julia/juliac/juliac.jl $(build_datarootdir)/julia/juliac/juliac-buildscript.jl $(build_datarootdir)/julia/juliac/juliac-trim-base.jl $(build_datarootdir)/julia/juliac/juliac-trim-stdlib.jl $(build_datarootdir)/julia/juliac/Artifacts.toml
julia-base: julia-deps $(build_sysconfdir)/julia/startup.jl $(build_man1dir)/julia.1 $(build_datarootdir)/julia/julia-config.jl $(build_datarootdir)/julia/juliac/juliac.jl $(build_datarootdir)/julia/juliac/abi_export.jl $(build_datarootdir)/julia/juliac/juliac-buildscript.jl $(build_datarootdir)/julia/juliac/juliac-trim-base.jl $(build_datarootdir)/julia/juliac/juliac-trim-stdlib.jl $(build_datarootdir)/julia/juliac/Artifacts.toml
@$(MAKE) $(QUIET_MAKE) -C $(BUILDROOT)/base

julia-libccalltest: julia-deps
Expand Down
219 changes: 219 additions & 0 deletions contrib/juliac/abi_export.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
const C_friendly_types = Base.IdSet{Any}([ # a few of these are redundant to make it easier to maintain
Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64, Float32, Float64, Bool,
Cvoid, Cint, Cshort, Clong, Cuint, Cushort, Culong, Cssize_t, Csize_t,
Cchar, Cwchar_t, Cstring, Cwstring,
RawFD,
])

function recursively_add_types!(types::Base.IdSet{DataType}, @nospecialize(T::DataType))
T in types && return types
while T.name === Ptr.body.name
push!(types, T)
T = T.parameters[1] # unwrap Ptr{...}
T in types && return types
end
if T.name.module === Core && T ∉ C_friendly_types
error("invalid type for juliac: ", T) # exclude internals (they may change)
end
push!(types, T)
for list in (T.parameters, fieldtypes(T))
for S in list
recursively_add_types!(types, S)
end
end
return types
end

struct TypeEmitter
io::IO
type_ids::IdDict{Any,Int}
end

function escape_string_json(s::AbstractString)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC there isn't a "meaningful" test of this escaping.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we have a number of copies of this now running around (test_print_str_escape_json, json_repr). Can we potentially combine them, or at least put them all in a single file to include as needed, so that we can make sure they all use the same logic?

iob = IOBuffer()
print(iob, '"')
for c in s
if c == '"'
print(iob, "\\\"")
elseif c == '\\'
print(iob, "\\\\")
elseif c == '\b'
print(iob, "\\b")
elseif c == '\f'
print(iob, "\\f")
elseif c == '\n'
print(iob, "\\n")
elseif c == '\r'
print(iob, "\\r")
elseif c == '\t'
print(iob, "\\t")
elseif '\x00' <= c <= '\x1f'
print(iob, "\\u", lpad(string(UInt16(c), base=16), 4, '0'))
else
@assert isvalid(c) "invalid unicode character"
print(iob, c)
end
end
print(iob, '"')
return String(take!(iob))
end

function type_name_json(@nospecialize(dt::DataType))
return escape_string_json(repr(dt))
end

function field_name_json(@nospecialize(dt::DataType), field::Int)
name = String(fieldname(dt, field))
return escape_string_json(name)
end

function emit_pointer_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
pointee_type_id = ctx.type_ids[dt.parameters[1]]
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
indented_println("{")
indented_println(" \"id\": ", ctx.type_ids[dt], ",")
indented_println(" \"kind\": \"pointer\",")
indented_println(" \"name\": ", type_name_json(dt), ",")
indented_println(" \"pointee_type_id\": ", pointee_type_id)
print(ctx.io, " " ^ indent, "}")
end
end

function emit_field_info!(ctx::TypeEmitter, @nospecialize(dt::DataType), field::Int; indent::Int = 0)
desc = Base.DataTypeFieldDesc(dt)[field]
type_id = ctx.type_ids[fieldtype(dt, field)]
print(ctx.io, " " ^ indent)
print(ctx.io, "{")
print(ctx.io, " \"name\": ", field_name_json(dt, field), ",")
print(ctx.io, " \"type_id\": ", type_id, ",")
print(ctx.io, " \"offset\": ", desc.offset)
print(ctx.io, " }")
end

function emit_struct_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
type_id = ctx.type_ids[dt]
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
indented_println("{")
indented_println(" \"id\": ", type_id, ",")
indented_println(" \"kind\": \"struct\",")
indented_println(" \"name\": ", type_name_json(dt), ",")
indented_println(" \"size\": ", Core.sizeof(dt), ",")
indented_println(" \"alignment\": ", Base.datatype_alignment(dt), ",")
indented_println(" \"fields\": [")
for i = 1:Base.datatype_nfields(dt)
emit_field_info!(ctx, dt, i; indent = indent + 4)
println(ctx.io, i == Base.datatype_nfields(dt) ? "" : ",")
end
indented_println(" ]")
print(ctx.io, " " ^ indent, "}")
end
end

function emit_primitive_type!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
type_id = ctx.type_ids[dt]
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
indented_println("{")
indented_println(" \"id\": ", type_id, ",")
indented_println(" \"kind\": \"primitive\",")
indented_println(" \"name\": ", type_name_json(dt), ",")
indented_println(" \"signed\": ", (dt <: Signed), ",")
indented_println(" \"bits\": ", 8 * Base.packedsize(dt), ",") # size for reinterpret / in-register
indented_println(" \"size\": ", Base.aligned_sizeof(dt), ",") # size with padding / in-memory
indented_println(" \"alignment\": ", Base.datatype_alignment(dt))
print(ctx.io, " " ^ indent, "}")
end
end

function emit_type_info!(ctx::TypeEmitter, @nospecialize(dt::DataType); indent::Int = 0)
if dt.name === Ptr.body.name
emit_pointer_info!(ctx, dt; indent)
elseif Base.isprimitivetype(dt)
emit_primitive_type!(ctx, dt; indent)
else
emit_struct_info!(ctx, dt; indent)
end
end

function emit_method_info!(ctx::TypeEmitter, method::Core.Method; indent::Int = 0)
(rt, sig) = method.ccallable
(name, symbol) = let
symbol = length(method.ccallable) > 2 ? Symbol(method.ccallable[3]) : method.name
iob = IOBuffer()
print(IOContext(iob, :print_method_signature_only => true), method)
str = String(take!(iob))
if symbol !== method.name && startswith(str, String(method.name))
# Make a best-effort attempt to use the exported name
#
# Note: the `startswith` check is to make sure we support 'functor's in arg0,
# which Base.@ccallable supports as long as they are singletons.
str = replace(str, String(method.name) => String(symbol); count = 1)
end
(str, String(symbol))
end

argnames = String.(Base.method_argnames(method))
let indented_println(args...) = println(ctx.io, " " ^ indent, args...)
indented_println("{")
indented_println(" \"symbol\": ", escape_string_json(symbol), ",")
indented_println(" \"name\": ", escape_string_json(name), ",")
indented_println(" \"arguments\": [")
for i in 2:length(sig.parameters)
print(ctx.io, " " ^ (indent + 4))
print(ctx.io, "{")
print(ctx.io, " \"name\": ", escape_string_json(argnames[i]), ",")
print(ctx.io, " \"type_id\": ", ctx.type_ids[sig.parameters[i]])
println(ctx.io, i == length(sig.parameters) ? " }" : " },")
end
indented_println(" ],")
indented_println(" \"returns\": { \"type_id\": ", ctx.type_ids[rt], " }")
print(ctx.io, " " ^ indent, "}")
end
end

function emit_abi_info!(ctx::TypeEmitter, exported::Vector{Core.Method}, types::IdSet{DataType})
println(ctx.io, "{")

# assign an ID to each type, so that we can refer to them
for (i, T) in enumerate(types)
ctx.type_ids[T] = i
end

# print exported functions
println(ctx.io, " \"functions\": [")
for (i, method) in enumerate(exported)
emit_method_info!(ctx, method; indent = 4)
println(ctx.io, i == length(exported) ? "" : ",")
end
println(ctx.io, " ],")

# print type / structure information
println(ctx.io, " \"types\": [")
for (i, T) in enumerate(types)
emit_type_info!(ctx, T; indent = 4)
println(ctx.io, i == length(types) ? "" : ",")
end
println(ctx.io, " ]")

println(ctx.io, "}")
end

function write_abi_metadata(io::IO)
types = Base.IdSet{DataType}()

# discover all exported methods + any types they reference
exported = Core.Method[]
Base.visit(Core.methodtable) do method
if isdefined(method, :ccallable)
push!(exported, method)
(rt, sig) = method.ccallable
for T in sig.parameters[2:end]
recursively_add_types!(types, T)
end
recursively_add_types!(types, rt)
end
end

# print the discovered ABI info
ctx = TypeEmitter(io, IdDict{Any,Int}())
emit_abi_info!(ctx, exported, types)
end
10 changes: 10 additions & 0 deletions contrib/juliac/juliac-buildscript.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ if Base.JLOptions().trim != 0
include(joinpath(@__DIR__, "juliac-trim-base.jl"))
end

include(joinpath(@__DIR__, "abi_export.jl"))

# Load user code

import Base.Experimental.entrypoint
Expand Down Expand Up @@ -78,6 +80,14 @@ let include_result = Base.include(Main, ARGS[1])
if ARGS[3] == "true"
ccall(:jl_add_ccallable_entrypoints, Cvoid, ())
end

# Export info about entrypoints and structs needed to create header files
if length(ARGS) >= 4
abi_export = ARGS[4]
open(abi_export, "w") do io
write_abi_metadata(io)
end
end
end

if Base.JLOptions().trim != 0
Expand Down
12 changes: 11 additions & 1 deletion contrib/juliac/juliac.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ julia_cmd = `$(Base.julia_cmd()) --startup-file=no --history-file=no`
cpu_target = get(ENV, "JULIA_CPU_TARGET", nothing)
julia_cmd_target = `$(Base.julia_cmd(;cpu_target)) --startup-file=no --history-file=no`
output_type = nothing # exe, sharedlib, sysimage
abi_export_file = nothing
outname = nothing
file = nothing
add_ccallables = false
Expand All @@ -25,6 +26,7 @@ if help !== nothing
"""
Usage: julia juliac.jl [--output-exe | --output-lib | --output-sysimage] <name> [options] <file.jl>
--experimental --trim=<no,safe,unsafe,unsafe-warn> Only output code statically determined to be reachable
--export-abi <file> Emit type / function information for the ABI (in JSON format)
--compile-ccallable Include all methods marked `@ccallable` in output
--relative-rpath Configure the library / executable to lookup all required libraries in an adjacent "julia/" folder
--verbose Request verbose output
Expand Down Expand Up @@ -97,6 +99,10 @@ let i = 1
i == length(ARGS) && error("Output specifier requires an argument")
global outname = ARGS[i+1]
i += 1
elseif arg == "--export-abi"
i == length(ARGS) && error("Output specifier requires an argument")
global abi_export_file = ARGS[i+1]
i += 1
elseif arg == "--compile-ccallable"
global add_ccallables = true
elseif arg == "--verbose"
Expand Down Expand Up @@ -171,7 +177,11 @@ function compile_products(enable_trim::Bool)
end

# Compile the Julia code
cmd = addenv(`$julia_cmd_target $project --output-o $img_path --output-incremental=no $strip_args $julia_args $(joinpath(@__DIR__,"juliac-buildscript.jl")) $absfile $output_type $add_ccallables`, "OPENBLAS_NUM_THREADS" => 1, "JULIA_NUM_THREADS" => 1)
args = String[absfile, output_type, string(add_ccallables)]
if abi_export_file !== nothing
push!(args, abi_export_file)
end
cmd = addenv(`$julia_cmd_target $project --output-o $img_path --output-incremental=no $strip_args $julia_args $(joinpath(@__DIR__,"juliac-buildscript.jl")) $(args)`, "OPENBLAS_NUM_THREADS" => 1, "JULIA_NUM_THREADS" => 1)
verbose && println("Running: $cmd")
if !success(pipeline(cmd; stdout, stderr))
println(stderr, "\nFailed to compile $file")
Expand Down
1 change: 1 addition & 0 deletions test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ gcext:

trimming:
@$(MAKE) -C $(SRCDIR)/$@ check $(TRIMMING_ARGS)
@$(MAKE) -C $(SRCDIR)/$@ clean $(TRIMMING_ARGS)

clangsa:
@$(MAKE) -C $(SRCDIR)/$@
Expand Down
12 changes: 9 additions & 3 deletions test/trimming/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ JULIAC_BUILDSCRIPT := $(shell $(JULIA) -e 'print(joinpath(Sys.BINDIR, Base.DATAR

#=============================================================================

release: $(BIN)/hello$(EXE) $(BIN)/basic_jll$(EXE)
release: $(BIN)/hello$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/capplication$(EXE)

$(BIN)/hello-o.a: $(SRCDIR)/hello.jl $(JULIAC_BUILDSCRIPT)
$(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --output-o $@ --output-incremental=no --strip-ir --strip-metadata --experimental --trim $(JULIAC_BUILDSCRIPT) $< --output-exe true
Expand All @@ -42,17 +42,23 @@ $(BIN)/basic_jll-o.a: $(SRCDIR)/basic_jll.jl $(JULIAC_BUILDSCRIPT)
$(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --project=$(SRCDIR) -e "using Pkg; Pkg.instantiate()"
$(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --project=$(SRCDIR) --output-o $@ --output-incremental=no --strip-ir --strip-metadata --experimental --trim $(JULIAC_BUILDSCRIPT) $< --output-exe true

$(BIN)/libsimple-o.a: $(SRCDIR)/libsimple.jl $(JULIAC_BUILDSCRIPT)
$(JULIA) -t 1 -J $(JULIA_LIBDIR)/julia/sys.$(SHLIB_EXT) --startup-file=no --history-file=no --output-o $@ --output-incremental=no --strip-ir --strip-metadata --experimental --trim $(JULIAC_BUILDSCRIPT) $< --output-lib true $(BIN)/bindinginfo_libsimple.json

$(BIN)/hello$(EXE): $(BIN)/hello-o.a
$(CC) -o $@ $(WHOLE_ARCHIVE) $< $(NO_WHOLE_ARCHIVE) $(CPPFLAGS_ADD) $(CPPFLAGS) $(CFLAGS_ADD) $(CFLAGS) $(LDFLAGS_ADD) $(LDFLAGS)

$(BIN)/basic_jll$(EXE): $(BIN)/basic_jll-o.a
$(CC) -o $@ $(WHOLE_ARCHIVE) $< $(NO_WHOLE_ARCHIVE) $(CPPFLAGS_ADD) $(CPPFLAGS) $(CFLAGS_ADD) $(CFLAGS) $(LDFLAGS_ADD) $(LDFLAGS)

check: $(BIN)/hello$(EXE) $(BIN)/basic_jll$(EXE)
$(BIN)/capplication$(EXE): $(SRCDIR)/capplication.c $(SRCDIR)/libsimple.h $(BIN)/libsimple-o.a
$(CC) -I$(BIN) -I$(SRCDIR) -I$(JULIA_LIBDIR) -o $@ $< -Wl,--whole-archive $(BIN)/libsimple-o.a -Wl,--no-whole-archive $(LDFLAGS_ADD) $(LDFLAGS) $(CPPFLAGS_ADD) $(CPPFLAGS) $(CFLAGS_ADD) $(CFLAGS)

check: $(BIN)/hello$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/capplication$(EXE)
$(JULIA) --depwarn=error $(SRCDIR)/trimming.jl $<

clean:
-rm -f $(BIN)/hello$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/hello-o.a $(BIN)/basic_jll-o.a
-rm -f $(BIN)/hello$(EXE) $(BIN)/basic_jll$(EXE) $(BIN)/hello-o.a $(BIN)/basic_jll-o.a $(BIN)/libsimple-o.a $(BIN)/libsimple.$(SHLIB_EXT) $(BIN)/capplication$(EXE) $(BIN)/bindinginfo_libsimple.json

.PHONY: release clean check

Expand Down
20 changes: 20 additions & 0 deletions test/trimming/capplication.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <stdio.h>
#include "libsimple.h"

int main() {
// Example usage of the functions defined in libsimple.h
CVectorPair_Float32 vecPair;
vecPair.from.length = 3;
vecPair.from.data = (float[]){1.0f, 2.0f, 3.0f};
vecPair.to.length = 3;
vecPair.to.data = (float[]){4.0f, 5.0f, 6.0f};

float sum = copyto_and_sum(vecPair);
printf("Sum of copied values: %f\n", sum);

MyTwoVec list[] = {{1, 2}, {5, 5}, {3, 4}};
int32_t count = countsame(list, 3);
printf("Count of same vectors: %d\n", count);

return 0;
}
31 changes: 31 additions & 0 deletions test/trimming/libsimple.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#ifndef JULIALIB_LIBSIMPLE_H
#define JULIALIB_LIBSIMPLE_H
#include <stddef.h>
#include <stdint.h>
#include <stdbool.h>

struct CTree_Float64;
typedef struct CVector_CTree_Float64 {
int32_t length;
struct CTree_Float64* data;
} CVector_CTree_Float64;
typedef struct CTree_Float64 {
CVector_CTree_Float64 children;
} CTree_Float64;
typedef struct MyTwoVec {
int32_t x;
int32_t y;
} MyTwoVec;
typedef struct CVector_Float32 {
int32_t length;
float* data;
} CVector_Float32;
typedef struct CVectorPair_Float32 {
CVector_Float32 from;
CVector_Float32 to;
} CVectorPair_Float32;

float copyto_and_sum(CVectorPair_Float32 fromto);
int64_t tree_size(CTree_Float64 tree);
int32_t countsame(MyTwoVec* list, int32_t n);
#endif // JULIALIB_LIBSIMPLE_H
Loading