Skip to content

Commit 0bfc3ac

Browse files
authored
fix: ensure lean_initialize is called when Lean is only privately imported (#14505)
This PR fixes a compiler issue where private imports of the `Lean` library could lead to segfaults by ensuring the necessary call to `lean_initialize` happens in each module's initializer when necessary. As a follow-up clean up, the call to `lean_initialize_runtime_module` is made implicit as well, meaning users of Lean as an FFI library do not need to call these functions themselves anymore.
1 parent 3b7f377 commit 0bfc3ac

10 files changed

Lines changed: 74 additions & 7 deletions

File tree

src/Lean/Compiler/LCNF/EmitC.lean

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,6 +1015,21 @@ def emitInitFn (phases : IRPhases) : EmitM Unit := do
10151015
let fn := mkModuleInitializationFunctionName (phases := if phases == .all then .all else if imp.isMeta then .runtime else phases) imp.module pkg?
10161016
emitLn s!"lean_object* {fn}(uint8_t builtin);"
10171017
return some fn
1018+
-- Every module initializes the runtime for itself so that external users of a Lean library do
1019+
-- not have to. Modules using the `Lean` package call the full `lean_initialize` instead as
1020+
-- module visibility can hide such an import from downstream modules (including the final
1021+
-- executable's root); the `Lean` modules themselves are initialized by `lean_initialize` and so
1022+
-- must not call it themselves.
1023+
let modName ← getModName
1024+
let leanInitFn? :=
1025+
if phases == .comptime then
1026+
none
1027+
else if usesModuleFrom env `Lean && !(`Lean).isPrefixOf modName then
1028+
some "lean_initialize"
1029+
else
1030+
some "lean_initialize_runtime_module"
1031+
if let some fn := leanInitFn? then
1032+
emitLn s!"void {fn}();"
10181033
let initialized := s!"_G_{mkModuleInitializationPrefix phases}initialized"
10191034
emitLns [
10201035
s!"static bool {initialized} = false;",
@@ -1023,6 +1038,8 @@ def emitInitFn (phases : IRPhases) : EmitM Unit := do
10231038
s!"if ({initialized}) return lean_io_result_mk_ok(lean_box(0));",
10241039
s!"{initialized} = true;"
10251040
]
1041+
if let some fn := leanInitFn? then
1042+
emitLn s!"{fn}();"
10261043
impInitFns.forM fun fn => do
10271044
withErrRet do
10281045
emit s!"{fn}(builtin)"
@@ -1077,10 +1094,8 @@ where
10771094
if ps.size != 1 && ps.size != 2 then
10781095
throwError "invalid main function, incorrect arity when generating code"
10791096
let env ← getEnv
1080-
let usesLeanAPI := usesModuleFrom env `Lean
10811097
emitLns [
10821098
"char ** lean_setup_args(int argc, char ** argv);",
1083-
if usesLeanAPI then "void lean_initialize();" else "void lean_initialize_runtime_module();",
10841099
"#if defined(WIN32) || defined(_WIN32)",
10851100
"#include <windows.h>",
10861101
"#endif",
@@ -1109,7 +1124,6 @@ where
11091124
"#endif",
11101125
" lean_object* res;",
11111126
" argv = lean_setup_args(argc, argv);",
1112-
if usesLeanAPI then " lean_initialize();" else " lean_initialize_runtime_module();",
11131127
s!" res = {← getModInitFn (phases := if env.header.isModule then .runtime else .all)}(1 /* builtin */);",
11141128
" lean_io_mark_end_initialization();",
11151129
" if (lean_io_result_is_ok(res)) {",

src/initialize/init.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@ extern "C" object* initialize_Init(uint8_t);
2020
extern "C" object* initialize_Std(uint8_t);
2121
extern "C" object* initialize_Lean(uint8_t);
2222

23+
static bool g_initialized = false;
2324
/* Initializes the Lean runtime. Before executing any code which uses the Lean package,
2425
you must first call this function, and then `lean::io_mark_end_initialization`. In between
25-
these two calls, you may also have to run additional initializers for your own modules. */
26+
these two calls, you may also have to run additional initializers for your own modules.
27+
28+
This function is, and needs to stay, idempotent; it is called by the generated initializer of every
29+
module using the Lean package, which may happen multiple times in a single executable. */
2630
extern "C" LEAN_EXPORT void lean_initialize() {
31+
if (g_initialized)
32+
return;
33+
g_initialized = true;
2734
save_stack_info();
2835
initialize_util_module();
2936
uint8_t builtin = 1;

src/runtime/init_module.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ Author: Leonardo de Moura
1616
#include "runtime/libuv.h"
1717

1818
namespace lean {
19+
// idempotent as it may be called both by the generated `main` and, via `lean_initialize`,
20+
// by generated module initializers
21+
static bool g_initialized = false;
1922
extern "C" LEAN_EXPORT void lean_initialize_runtime_module() {
23+
if (g_initialized)
24+
return;
25+
g_initialized = true;
2026
initialize_alloc();
2127
initialize_debug();
2228
initialize_object();

tests/lake/examples/reverse-ffi/main.c

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
extern uint64_t my_length(lean_obj_arg);
55

66
// see https://lean-lang.org/doc/reference/latest/find/?domain=Verso.Genre.Manual.section&name=ffi-initialization
7-
extern void lean_initialize_runtime_module();
8-
extern void lean_initialize();
97
extern void lean_io_mark_end_initialization();
108
extern lean_object * initialize_rffi_RFFI(uint8_t builtin);
119

1210
int main() {
13-
lean_initialize_runtime_module();
1411
lean_object * res;
1512
// use same default as for Lean executables
1613
uint8_t builtin = 1;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.lake
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module
2+
3+
import Lean.Environment
4+
5+
/-!
6+
Uses the `Lean` package through a *private* `import`, so downstream modules do not see `Lean` in
7+
this module's public interface.
8+
-/
9+
10+
public def checkEmptyEnv : IO Bool := do
11+
let env ← Lean.mkEmptyEnvironment
12+
return (env.find? `Nat).isNone
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module
2+
3+
import Foo
4+
5+
/-!
6+
Regression test: this executable root does not visibly import `Lean` (it is only privately imported
7+
by `Foo`), but the linked binary still contains and initializes `Lean` code, so the generated code
8+
must fully initialize the Lean runtime (`lean_initialize`) before running `main`. Getting this
9+
wrong makes the executable crash at startup.
10+
-/
11+
12+
public def main : IO Unit := do
13+
IO.println s!"empty env has no Nat: {← checkEmptyEnv}"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
name = "exe-private-lean-import"
2+
defaultTargets = ["main"]
3+
4+
[[lean_lib]]
5+
name = "Foo"
6+
7+
[[lean_exe]]
8+
name = "main"
9+
root = "Main"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../build/release/stage1
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
rm -rf .lake
2+
lake build
3+
4+
# Without full initialization of the Lean package (which is linked into the executable even though
5+
# it is not visibly imported into the root module), running the executable crashes.
6+
capture ./.lake/build/bin/main
7+
check_out_contains "empty env has no Nat: true"

0 commit comments

Comments
 (0)