Skip to content

Commit 3319bce

Browse files
gbaraldiclaude
andcommitted
Use jl_parse_opts for jl_options shim instead of manual struct assignment
Instead of generating C code that manually assigns jl_options struct fields, call jl_parse_opts with a synthetic argv. This reuses Julia's own option parsing logic and avoids duplicating the parsing for threads, handle-signals, etc. The constructor saves/restores getopt global state and image_file to avoid interfering with the host process. Options are validated at compile time by running a Julia subprocess with the same flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2775b3d commit 3319bce

File tree

3 files changed

+63
-127
lines changed

3 files changed

+63
-127
lines changed

src/compiling.jl

Lines changed: 26 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ function compile_products(recipe::ImageRecipe)
6161
recipe.add_ccallables = true
6262
end
6363
# Default: disable signal handlers and limit to single thread for shared libraries
64-
if recipe.output_type == "--output-lib" && isempty(recipe.jl_options)
65-
recipe.jl_options["handle-signals"] = "no"
66-
recipe.jl_options["threads"] = "1"
64+
if recipe.output_type == "--output-lib"
65+
get!(recipe.jl_options, "handle-signals", "no")
66+
get!(recipe.jl_options, "threads", "1")
6767
end
6868
if recipe.cpu_target === nothing
6969
recipe.cpu_target = get(ENV,"JULIA_CPU_TARGET", nothing)
@@ -214,79 +214,16 @@ function _validate_jl_options(jl_options::Dict{String,String})
214214
end
215215

216216
"""
217-
Parse `--jl-option handle-signals=yes|no` into C assignments.
218-
Mirrors Julia's `--handle-signals` parsing in `jloptions.c`.
219-
"""
220-
function _emit_handle_signals(io::IO, value::String)
221-
if value == "yes"
222-
println(io, "opts->handle_signals = JL_OPTIONS_HANDLE_SIGNALS_ON;")
223-
elseif value == "no"
224-
println(io, "opts->handle_signals = JL_OPTIONS_HANDLE_SIGNALS_OFF;")
225-
else
226-
error("Invalid value for handle-signals: \"$value\". Expected \"yes\" or \"no\".")
227-
end
228-
end
229-
230-
"""
231-
Parse `--jl-option threads=N[,M]` into C assignments.
232-
Mirrors Julia's `--threads` / `-t` parsing in `jloptions.c`:
233-
- `N` → N threads in default pool; if N==1, 1 pool; else 2 pools (+ 1 interactive)
234-
- `N,M` → N default + M interactive; if M==0, 1 pool; else 2 pools
235-
- `N,auto` → N default + 1 interactive, 2 pools
236-
237-
Does not support `auto` for N (libraries should specify a concrete thread count).
238-
"""
239-
function _emit_threads(io::IO, value::String)
240-
parts = split(value, ','; limit=2)
241-
nthreads_str = parts[1]
242-
nthreads_str == "auto" && error("\"auto\" is not supported for threads in --jl-option; specify a concrete count.")
243-
nthreads = tryparse(Int, nthreads_str)
244-
nthreads === nothing && error("Invalid thread count: \"$nthreads_str\"")
245-
nthreads < 1 && error("Thread count must be >= 1, got $nthreads")
246-
247-
if length(parts) == 2
248-
# N,M or N,auto
249-
ipart = parts[2]
250-
if ipart == "auto"
251-
nthreadsi = 1
252-
else
253-
nthreadsi = tryparse(Int, ipart)
254-
nthreadsi === nothing && error("Invalid interactive thread count: \"$ipart\"")
255-
nthreadsi < 0 && error("Interactive thread count must be >= 0, got $nthreadsi")
256-
end
257-
elseif nthreads == 1
258-
# Like Julia: 1 thread → no interactive pool
259-
nthreadsi = 0
260-
else
261-
# Like Julia: N>1 → add 1 interactive thread
262-
nthreadsi = 1
263-
end
264-
265-
nthreadpools = nthreadsi == 0 ? 1 : 2
266-
total = nthreads + nthreadsi
267-
268-
println(io, "opts->nthreads = $total;")
269-
println(io, "opts->nthreadpools = $nthreadpools;")
270-
println(io, "{")
271-
println(io, " int16_t *ntpp = (int16_t *)malloc($nthreadpools * sizeof(int16_t));")
272-
println(io, " if (ntpp) {")
273-
println(io, " ntpp[0] = (int16_t)$nthreads;")
274-
if nthreadpools == 2
275-
println(io, " ntpp[1] = (int16_t)$nthreadsi;")
276-
end
277-
println(io, " opts->nthreads_per_pool = ntpp;")
278-
println(io, " }")
279-
println(io, "}")
280-
end
281-
282-
"""
283-
Generate and compile a C shim that overrides `jl_options` fields via an
284-
`__attribute__((constructor))`.
217+
Generate and compile a C shim that calls `jl_parse_opts` with a synthetic
218+
argv in an `__attribute__((constructor))`.
285219
286220
The boilerplate C code lives in `scripts/juliac-jl-options-shim.c` and
287-
`#include`s a generated `juliac-jl-options-body.h` containing just the
288-
option assignments. See the shim source for how `jl_options` is resolved
289-
on each platform.
221+
`#include`s a generated `juliac-jl-options-body.h` containing the argv
222+
string literals.
223+
224+
After compilation, links the shim into a trivial executable and runs it
225+
to validate that `jl_parse_opts` accepts the options. This catches invalid
226+
values at compile time rather than at load time.
290227
291228
Returns the path to the compiled object file.
292229
"""
@@ -297,14 +234,11 @@ function _compile_jl_options_shim(jl_options::Dict{String,String}; verbose::Bool
297234
shim_src = joinpath(JuliaC.SCRIPTS_DIR, "juliac-jl-options-shim.c")
298235
body_hdr = joinpath(tmpdir, "juliac-jl-options-body.h")
299236
init_obj = joinpath(tmpdir, "juliac-jl-options-init.o")
300-
# Generate only the option-assignment body
237+
# Generate argv entries for jl_parse_opts
301238
open(body_hdr, "w") do io
302-
println(io, "/* Generated by JuliaC — jl_options field assignments */")
303-
if haskey(jl_options, "handle-signals")
304-
_emit_handle_signals(io, jl_options["handle-signals"])
305-
end
306-
if haskey(jl_options, "threads")
307-
_emit_threads(io, jl_options["threads"])
239+
println(io, "/* Generated by JuliaC — jl_parse_opts argv entries */")
240+
for (key, value) in jl_options
241+
println(io, "\"--$(key)=$(value)\",")
308242
end
309243
end
310244
verbose && println("Generated jl_options body: $body_hdr")
@@ -320,5 +254,16 @@ function _compile_jl_options_shim(jl_options::Dict{String,String}; verbose::Bool
320254
catch e
321255
error("jl_options init shim compilation failed: ", e)
322256
end
257+
# Validate by running julia with the same flags.
258+
julia_bin = joinpath(Sys.BINDIR, "julia")
259+
validate_cmd = `$julia_bin`
260+
for (key, value) in jl_options
261+
validate_cmd = `$validate_cmd --$(key)=$(value)`
262+
end
263+
validate_cmd = `$validate_cmd -e ""`
264+
verbose && println("Validating jl_options: $validate_cmd")
265+
if !success(pipeline(validate_cmd; stdout=devnull, stderr))
266+
error("Invalid --jl-option values. Check that option values match Julia CLI syntax (e.g. --handle-signals=yes|no, --threads=N[,M]).")
267+
end
323268
return init_obj
324269
end

src/scripts/juliac-jl-options-shim.c

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,50 +3,49 @@
33
*
44
* Sets jl_options fields before jl_init via __attribute__((constructor)).
55
*
6-
* On macOS, dlsym with a handle from dlopen searches that image and its
7-
* dependencies, so we use dladdr/dlopen/dlsym to resolve jl_options from
8-
* our own libjulia dependency.
6+
* Calls jl_parse_opts with a synthetic argv to reuse Julia's own option
7+
* parsing logic. Saves and restores getopt global state and image_file
8+
* to avoid interfering with the host process.
99
*
10-
* On Linux, dlopen(RTLD_NOLOAD) on the main executable returns NULL, so
11-
* we use RTLD_DEFAULT which searches all loaded shared objects. This is
12-
* safe because our libjulia is the only one loaded at constructor time.
13-
*
14-
* On Windows, DLL import resolution is per-module, so &jl_options already
15-
* resolves to the correct instance from our libjulia dependency.
16-
*
17-
* The actual option assignments are generated by JuliaC and included via
10+
* The argv entries are generated by JuliaC and included via
1811
* "juliac-jl-options-body.h".
1912
*/
2013

21-
#define _GNU_SOURCE
22-
2314
#include <julia.h>
2415
#include <stdlib.h>
25-
26-
#ifdef _WIN32
27-
#include <windows.h>
28-
#else
29-
#include <dlfcn.h>
30-
#endif
16+
#include <getopt.h>
3117

3218
__attribute__((constructor))
3319
static void juliac_set_jl_options(void) {
34-
#ifdef _WIN32
35-
jl_options_t *opts = &jl_options;
36-
#elif defined(__APPLE__)
37-
Dl_info info;
38-
if (!dladdr((void *)&juliac_set_jl_options, &info)) return;
39-
void *self = dlopen(info.dli_fname, RTLD_NOLOAD | RTLD_NOW);
40-
if (!self) return;
41-
jl_options_t *opts = (jl_options_t *)dlsym(self, "jl_options");
42-
dlclose(self);
43-
if (!opts) return;
44-
#else
45-
/* Linux: RTLD_DEFAULT searches all loaded shared objects */
46-
jl_options_t *opts = (jl_options_t *)dlsym(RTLD_DEFAULT, "jl_options");
47-
if (!opts) return;
48-
#endif
49-
50-
/* Generated option assignments */
20+
/* Save getopt global state */
21+
int saved_optind = optind;
22+
int saved_opterr = opterr;
23+
int saved_optopt = optopt;
24+
char *saved_optarg = optarg;
25+
26+
/* Save fields that jl_parse_opts unconditionally overwrites */
27+
const char *saved_image_file = jl_options.image_file;
28+
29+
/* Synthetic argv with generated options */
30+
char *argv[] = {
31+
"juliac",
5132
#include "juliac-jl-options-body.h"
33+
NULL
34+
};
35+
int argc = (int)(sizeof(argv) / sizeof(argv[0])) - 1; /* exclude NULL */
36+
char **argvp = argv;
37+
38+
/* Reset getopt so it parses our argv from the beginning */
39+
optind = 0;
40+
41+
jl_parse_opts(&argc, &argvp);
42+
43+
/* Restore overwritten fields */
44+
jl_options.image_file = saved_image_file;
45+
46+
/* Restore getopt global state */
47+
optind = saved_optind;
48+
opterr = saved_opterr;
49+
optopt = saved_optopt;
50+
optarg = saved_optarg;
5251
}

test/programatic.jl

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,7 @@ end
338338
@test img.jl_options["handle-signals"] == "no"
339339
@test img.jl_options["threads"] == "1"
340340

341-
# User-provided jl_options should not be overwritten
341+
# User-provided jl_options should not be overwritten, other defaults still applied
342342
img2 = JuliaC.ImageRecipe(
343343
file = TEST_LIB_SRC,
344344
output_type = "--output-lib",
@@ -349,7 +349,7 @@ end
349349
)
350350
JuliaC.compile_products(img2)
351351
@test img2.jl_options["handle-signals"] == "yes"
352-
@test !haskey(img2.jl_options, "threads")
352+
@test img2.jl_options["threads"] == "1" # default still applied
353353

354354
# --output-exe should not auto-populate jl_options
355355
img3 = JuliaC.ImageRecipe(
@@ -380,14 +380,6 @@ end
380380
# Supported options should pass validation
381381
JuliaC._validate_jl_options(Dict("handle-signals" => "no"))
382382
JuliaC._validate_jl_options(Dict("threads" => "1"))
383-
384-
# Invalid handle-signals value should error
385-
@test_throws ErrorException JuliaC._emit_handle_signals(devnull, "maybe")
386-
387-
# Invalid threads value should error
388-
@test_throws ErrorException JuliaC._emit_threads(devnull, "auto")
389-
@test_throws ErrorException JuliaC._emit_threads(devnull, "0")
390-
@test_throws ErrorException JuliaC._emit_threads(devnull, "abc")
391383
end
392384

393385
@testset "jl_options applied at runtime" begin

0 commit comments

Comments
 (0)