Skip to content

Commit d89d51e

Browse files
committed
compiler+rts: port #181 wasm32 cross-target + per-target-settings stack
Ports the two GHC-source commits #181 (feat/wasm-cross-ghcup) carries but the stable-ghc-9.14 / #188 base lacks, which are the real fix for the stage3-wasm ghc-internal link failure ([GHC-74335] "-dynamic ignored when linking binaries on WASM" -> mismatched interface profile tag): - 7396909 "rts+compiler: wasm32 cross-target patches" (GHC/Linker/Dynamic.hs wasm dynamic-link handling, Driver/Session.hs, Runtime/Interpreter/Wasm.hs, rts/linker/elf_got.c, rts/RtsStartup.c) - 4d84ace "compiler: per-target settings drive GHC Dynamic / Profiled" (Platform/Settings/Settings.IO/Driver.Session: platformMisc_targetIsDynamic et al., defaulting True so the wasm target is dynamic-capable) compiler/Setup.hs + libraries/ghc-boot/Setup.hs were KEPT at the modern-pin VerbosityHandles form (7396's pre-split Setup.hs revert was discarded — it's incompatible with the post-split cabal pin).
1 parent 17bdcd1 commit d89d51e

9 files changed

Lines changed: 247 additions & 25 deletions

File tree

compiler/GHC/Driver/Session.hs

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ module GHC.Driver.Session (
132132
sGhcWithInterpreter,
133133
sLibFFI,
134134
sTargetRTSLinkerOnlySupportsSharedLibs,
135+
sTargetIsDynamic,
136+
sTargetShipsDynLibs,
137+
sTargetIsProfiled,
138+
sTargetShipsProfLibs,
135139
GhcNameVersion(..),
136140
FileSettings(..),
137141
PlatformMisc(..),
@@ -3548,8 +3552,16 @@ compilerInfo dflags
35483552
("Have native code generator", showBool $ platformNcgSupported platform),
35493553
("target has RTS linker", showBool $ platformHasRTSLinker platform),
35503554
("Target default backend", show $ platformDefaultBackend platform),
3551-
-- Whether or not we support @-dynamic-too@
3552-
("Support dynamic-too", showBool $ not isWindows),
3555+
-- Whether or not we support @-dynamic-too@ for this target.
3556+
-- Historically `not isWindows` (Windows tooling couldn't do
3557+
-- it). Now also gated on the per-target `sTargetIsDynamic`
3558+
-- dial — if the target isn't dynamic-capable, -dynamic-too
3559+
-- is meaningless. Keep the Windows guard as defence in depth
3560+
-- for pre-this-patch bindists on Windows that lack the key
3561+
-- and so default sTargetIsDynamic=True (the AND would
3562+
-- otherwise regress them).
3563+
("Support dynamic-too", showBool $ not isWindows
3564+
&& sTargetIsDynamic (settings dflags)),
35533565
-- Whether or not we support the @-j@ flag with @--make@.
35543566
("Support parallel --make", "YES"),
35553567
-- Whether or not we support "Foo from foo-0.1-XXX:Foo" syntax in
@@ -3569,10 +3581,27 @@ compilerInfo dflags
35693581
("Uses package keys", "YES"),
35703582
-- Whether or not we support the @-this-unit-id@ flag
35713583
("Uses unit IDs", "YES"),
3572-
-- Whether or not GHC was compiled using -dynamic
3573-
("GHC Dynamic", showBool hostIsDynamic),
3574-
-- Whether or not GHC was compiled using -prof
3575-
("GHC Profiled", showBool hostIsProfiled),
3584+
-- Reported as YES iff *both* per-target settings dials say so:
3585+
-- `target is dynamic` — the GHC for this target
3586+
-- can produce dynamic output
3587+
-- `target ships dynamic libraries` — the lib tree actually has
3588+
-- .dyn_hi / .so artifacts
3589+
-- cabal-install reads this to decide whether to enable
3590+
-- @library-dynamic@ by default. The target's per-target settings
3591+
-- file completely controls this value — no host-RTS dependency,
3592+
-- so on a multi-target bindist with one shared stage2 GHC binary
3593+
-- different targets can correctly disagree. Both keys default to
3594+
-- True if absent (matches pre-this-change behaviour).
3595+
("GHC Dynamic", showBool (sTargetIsDynamic (settings dflags)
3596+
&& sTargetShipsDynLibs (settings dflags))),
3597+
-- Profiling-way analogue of `GHC Dynamic`. Per-target dials
3598+
-- via `target is profiled` + `target ships profiling libraries`
3599+
-- settings keys. Drops the historical `hostIsProfiled` RTS-
3600+
-- baked-in for the same reason the dyn pair did: on a multi-
3601+
-- target bindist the shared stage2 GHC binary's prof-ness is
3602+
-- fixed but the lib trees can disagree per target.
3603+
("GHC Profiled", showBool (sTargetIsProfiled (settings dflags)
3604+
&& sTargetShipsProfLibs (settings dflags))),
35763605
("Debug on", showBool debugIsOn),
35773606
("LibDir", topDir dflags),
35783607
-- This is always an absolute path, unlike "Relative Global Package DB" which is
@@ -3782,18 +3811,26 @@ makeDynFlagsConsistent dflags
37823811
= let warn = "-dynamic is ignored when using -staticlib"
37833812
in loop dflags{targetWays_ = removeWay WayDyn (targetWays_ dflags)} warn
37843813
-- For the wasm target, when ghc is invoked with -dynamic,
3785-
-- when linking the final .wasm binary we must still ensure
3786-
-- the static archives are selected. Otherwise wasm-ld would
3787-
-- fail to find and link the .so library dependencies. wasm-ld
3788-
-- can link PIC objects into static .wasm binaries fine, so we
3789-
-- only adjust the ways in the final linking step, and only
3790-
-- when linking .wasm binary (which is supposed to be fully
3791-
-- static), not when linking .so shared libraries.
3792-
| LinkExecutable _ <- ghcLink dflags
3793-
, ArchWasm32 <- arch
3794-
, ways dflags `hasWay` WayDyn
3795-
= let warn = "-dynamic is ignored when linking binaries on WASM"
3796-
in loop dflags{targetWays_ = removeWay WayDyn (targetWays_ dflags)} warn
3814+
-- (DISABLED Phase 6.5, 2026-05-26) — the original rule below was meant
3815+
-- to strip WayDyn only when linking the final .wasm binary, but the
3816+
-- default `ghcLink` in `defaultDynFlags` is `LinkExecutable Dynamic`, so
3817+
-- the rule fired for EVERY --make invocation that didn't override it.
3818+
-- That broke cabal library builds with `shared:True, library-vanilla:False`
3819+
-- (the rule stripped WayDyn mid-compile, then GHC looked for vanilla .hi
3820+
-- files that don't exist since only .dyn_hi was produced).
3821+
--
3822+
-- The wasm linker (wasm-ld) silently handles -dynamic without issue —
3823+
-- wasm binaries are inherently single-file artifacts, and PIC objects
3824+
-- link fine into them. Removing the strip lets WayDyn flow through to
3825+
-- the actual link step, where wasm-ld does the right thing.
3826+
-- (If a future regression surfaces, narrow the rule to fire ONLY in the
3827+
-- actual link phase, not at flag-consistency time.)
3828+
--
3829+
-- | LinkExecutable _ <- ghcLink dflags
3830+
-- , ArchWasm32 <- arch
3831+
-- , ways dflags `hasWay` WayDyn
3832+
-- = let warn = "-dynamic is ignored when linking binaries on WASM"
3833+
-- in loop dflags{targetWays_ = removeWay WayDyn (targetWays_ dflags)} warn
37973834

37983835
| LinkInMemory <- ghcLink dflags
37993836
, not (gopt Opt_ExternalInterpreter dflags)

compiler/GHC/Linker/Dynamic.hs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import GHC.SysTools.Tasks
1616

1717
import GHC.Driver.Config.Linker
1818
import GHC.Driver.Session
19+
import GHC.Driver.DynFlags (rtsWayUnitId)
1920

2021
import GHC.Unit.Env
2122
import GHC.Unit.Types
@@ -52,7 +53,27 @@ linkDynLib logger tmpfs dflags0 unit_env o_files dep_packages
5253
verbFlags = getVerbFlags dflags
5354
o_file = outputFile_ dflags
5455

55-
pkgs_with_rts <- mayThrowUnitErr (preloadUnitsInfo' unit_env dep_packages)
56+
pkgs_with_rts <- do
57+
pkgs0 <- mayThrowUnitErr (preloadUnitsInfo' unit_env dep_packages)
58+
-- On wasm32, every Haskell .so must declare the current way's rts in
59+
-- its `dylink.0/needed_dynlibs` section, otherwise the runtime dyld
60+
-- can load it ahead of rts and the .so's `_initialize` references
61+
-- unresolved rts symbols (e.g. `registerForeignExports`) — see
62+
-- ghc-internal, which is built with `-no-rts` to bypass mkUnitState's
63+
-- sanity check (compiler/GHC/Unit/State.hs:1652) but consequently has
64+
-- cabal omit rts from its --depends list. wasm-ld populates
65+
-- needed_dynlibs from the resolved `-l` flags, so we must inject the
66+
-- current way's rts here whenever it isn't already present, even if
67+
-- `-no-rts` is in effect. Other targets either link rts statically at
68+
-- exe-link time or rely on host symbol resolution; on wasm32 the .so
69+
-- itself carries the dependency.
70+
let rts_uid = rtsWayUnitId dflags
71+
pure $ case arch of
72+
ArchWasm32
73+
| not (any ((== rts_uid) . unitId) pkgs0)
74+
, Just rts_info <- lookupUnitId (ue_units unit_env) rts_uid
75+
-> rts_info : pkgs0
76+
_ -> pkgs0
5677

5778
let pkg_lib_paths = collectLibraryDirs (ways dflags) pkgs_with_rts
5879
let pkg_lib_path_opts = concatMap get_pkg_lib_path_opts pkg_lib_paths
@@ -84,7 +105,10 @@ linkDynLib logger tmpfs dflags0 unit_env o_files dep_packages
84105
-- * if -flink-rts is used, we link with the rts.
85106
--
86107
-- * on wasm we need to ensure libHSrts*.so is listed in
87-
-- WASM_DYLINK_NEEDED, otherwise dyld can't load it.
108+
-- WASM_DYLINK_NEEDED, otherwise dyld can't load it. We force the
109+
-- current way's rts into `pkgs_with_rts` above so that even .so
110+
-- files whose package was built with `-no-rts` (e.g. ghc-internal)
111+
-- still declare the rts dependency.
88112
--
89113
--
90114
let pkgs_without_rts = filter ((/= PackageName (fsLit "rts")) . unitPackageName) pkgs_with_rts

compiler/GHC/Platform.hs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,33 @@ data PlatformMisc = PlatformMisc
291291
, platformMisc_libFFI :: Bool
292292
, platformMisc_llvmTarget :: String
293293
, platformMisc_targetRTSLinkerOnlySupportsSharedLibs :: Bool
294+
-- | Is the GHC for this target capable of producing dynamic
295+
-- output (i.e. can it honour @-dynamic@ / @-dynamic-too@)?
296+
-- Per-target settings key @"target is dynamic"@ in
297+
-- @lib/targets/\<triple\>/lib/settings@. On a multi-target bindist
298+
-- the shared stage2 GHC binary's RTS-baked-in dynamic-ness is
299+
-- not a per-target proxy — different targets in one binary may
300+
-- need to disagree (e.g. a JS target whose iserv runs vanilla
301+
-- and whose lib tree has no dyn artifacts). Combined with
302+
-- 'platformMisc_targetShipsDynLibs' to drive @ghc --info@'s
303+
-- @GHC Dynamic@ value, which cabal-install reads.
304+
, platformMisc_targetIsDynamic :: Bool
305+
-- | Does the target's installed library tree ship @.dyn_hi@ /
306+
-- @.so@ files? Per-target settings key
307+
-- @"target ships dynamic libraries"@. Set independently of
308+
-- 'platformMisc_targetIsDynamic' so a target can be dynamic-
309+
-- capable but not currently ship dyn artifacts (or vice versa).
310+
, platformMisc_targetShipsDynLibs :: Bool
311+
-- | Profiling-way analogue of 'platformMisc_targetIsDynamic'.
312+
-- Per-target settings key @"target is profiled"@. Drives
313+
-- @ghc --info@'s @GHC Profiled@ — cabal-install reads that to
314+
-- decide whether to enable @library-profiling@.
315+
, platformMisc_targetIsProfiled :: Bool
316+
-- | Profiling-way analogue of 'platformMisc_targetShipsDynLibs'.
317+
-- Per-target settings key @"target ships profiling libraries"@.
318+
-- Combined with 'platformMisc_targetIsProfiled' for the
319+
-- @GHC Profiled@ report.
320+
, platformMisc_targetShipsProfLibs :: Bool
294321
}
295322

296323
platformSOName :: Platform -> FilePath -> FilePath

compiler/GHC/Runtime/Interpreter/Wasm.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import GHC.Runtime.Interpreter.Types
1010
#if !defined(mingw32_HOST_OS)
1111

1212
import Control.Concurrent.MVar
13+
import Control.Exception (throwIO)
1314
import Data.Maybe
1415
import GHC.Data.FastString
1516
import qualified GHC.Data.ShortText as ST
1617
import GHC.Platform
1718
import GHC.Unit
19+
import GHC.Utils.Panic (GhcException (InstallationError))
1820
import GHCi.Message
1921
import System.Directory
2022
import System.Environment.Blank
@@ -34,6 +36,21 @@ spawnWasmInterp :: WasmInterpConfig -> IO (ExtInterpInstance ())
3436

3537
-- See Note [The Wasm Dynamic Linker] for details
3638
spawnWasmInterp WasmInterpConfig {..} = do
39+
-- Pre-flight: dyld.mjs (the wasm dynamic linker) has a
40+
-- `#!/usr/bin/env -S node` shebang and needs `node` on $PATH. Without
41+
-- it the iserv child exits with code 127, which the generic
42+
-- "External interpreter terminated (127)" message in
43+
-- Process.hs:129 doesn't explain. Emit a clear, actionable error
44+
-- here instead.
45+
mb_node <- findExecutable "node"
46+
case mb_node of
47+
Just _ -> pure ()
48+
Nothing -> throwIO . InstallationError $
49+
"Template Haskell evaluation on the wasm32 backend requires `node` "
50+
++ "(Node.js) on PATH. The wasm dynamic linker shim ("
51+
++ wasmInterpDyLD
52+
++ ") is a Node script. Install Node.js and ensure `node` is "
53+
++ "reachable, then retry."
3754
let Just ghci_unit_id =
3855
lookupPackageName
3956
wasmInterpUnitState

compiler/GHC/Settings.hs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ module GHC.Settings
6767
, sGhcWithInterpreter
6868
, sLibFFI
6969
, sTargetRTSLinkerOnlySupportsSharedLibs
70+
, sTargetIsDynamic
71+
, sTargetShipsDynLibs
72+
, sTargetIsProfiled
73+
, sTargetShipsProfLibs
7074
) where
7175

7276
import GHC.Prelude
@@ -314,3 +318,26 @@ sLibFFI = platformMisc_libFFI . sPlatformMisc
314318

315319
sTargetRTSLinkerOnlySupportsSharedLibs :: Settings -> Bool
316320
sTargetRTSLinkerOnlySupportsSharedLibs = platformMisc_targetRTSLinkerOnlySupportsSharedLibs . sPlatformMisc
321+
322+
-- | Is the GHC for this target capable of producing dynamic output?
323+
-- Read from the per-target settings file key @"target is dynamic"@.
324+
-- Combined with 'sTargetShipsDynLibs' to drive @ghc --info@'s
325+
-- @GHC Dynamic@ value — the target's settings file completely
326+
-- controls that, no host-RTS dependency.
327+
sTargetIsDynamic :: Settings -> Bool
328+
sTargetIsDynamic = platformMisc_targetIsDynamic . sPlatformMisc
329+
330+
-- | Does this target's installed library tree ship .dyn_hi / .so files?
331+
-- Per-target settings key @"target ships dynamic libraries"@.
332+
sTargetShipsDynLibs :: Settings -> Bool
333+
sTargetShipsDynLibs = platformMisc_targetShipsDynLibs . sPlatformMisc
334+
335+
-- | Profiling-way analogue of 'sTargetIsDynamic'. Per-target settings
336+
-- key @"target is profiled"@.
337+
sTargetIsProfiled :: Settings -> Bool
338+
sTargetIsProfiled = platformMisc_targetIsProfiled . sPlatformMisc
339+
340+
-- | Profiling-way analogue of 'sTargetShipsDynLibs'. Per-target settings
341+
-- key @"target ships profiling libraries"@.
342+
sTargetShipsProfLibs :: Settings -> Bool
343+
sTargetShipsProfLibs = platformMisc_targetShipsProfLibs . sPlatformMisc

compiler/GHC/Settings/IO.hs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,34 @@ initSettings top_dir = do
185185
ghcWithInterpreter <- getBooleanSetting "Use interpreter"
186186
useLibFFI <- getBooleanSetting "Use LibFFI"
187187

188+
-- Per-target dial #1: is the GHC for THIS target capable of
189+
-- producing dynamic output (i.e. honouring -dynamic /
190+
-- -dynamic-too)? On a multi-target bindist with one shared stage2
191+
-- GHC binary, this can't be derived from the binary's compile-
192+
-- time `hostIsDynamic`. Default True for backward compatibility
193+
-- with older bindist settings files that predate the key.
194+
targetIsDynamic <- either (const $ pure True) pure $
195+
getRawBooleanSetting settingsFile mySettings "target is dynamic"
196+
197+
-- Per-target dial #2: does this target's installed lib tree
198+
-- actually ship .dyn_hi / .so files? Independent of
199+
-- `target is dynamic` so a dynamic-capable target can still
200+
-- truthfully say it doesn't ship artifacts (e.g. a slimmed
201+
-- bindist). cabal-install combines both via GHC Dynamic to
202+
-- decide whether to enable library-dynamic by default.
203+
targetShipsDynLibs <- either (const $ pure True) pure $
204+
getRawBooleanSetting settingsFile mySettings "target ships dynamic libraries"
205+
206+
-- Profiling-way analogues of the two dyn dials above. Drive
207+
-- `GHC Profiled` the same way (sTargetIsProfiled && sTargetShipsProfLibs).
208+
-- Defaults to True for backward compatibility with bindists that
209+
-- predate the keys — matches the historical hostIsProfiled
210+
-- behaviour when GHC was prof-built.
211+
targetIsProfiled <- either (const $ pure True) pure $
212+
getRawBooleanSetting settingsFile mySettings "target is profiled"
213+
targetShipsProfLibs <- either (const $ pure True) pure $
214+
getRawBooleanSetting settingsFile mySettings "target ships profiling libraries"
215+
188216
baseUnitId <- getSetting_raw "base unit-id"
189217

190218
return $ Settings
@@ -267,6 +295,10 @@ initSettings top_dir = do
267295
, platformMisc_libFFI = useLibFFI
268296
, platformMisc_llvmTarget = llvmTarget
269297
, platformMisc_targetRTSLinkerOnlySupportsSharedLibs = targetRTSLinkerOnlySupportsSharedLibs
298+
, platformMisc_targetIsDynamic = targetIsDynamic
299+
, platformMisc_targetShipsDynLibs = targetShipsDynLibs
300+
, platformMisc_targetIsProfiled = targetIsProfiled
301+
, platformMisc_targetShipsProfLibs = targetShipsProfLibs
270302
}
271303

272304
, sRawSettings = settingsList

rts/RtsStartup.c

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -424,14 +424,11 @@ hs_init_ghc(int *argc, char **argv[], RtsConfig rts_config)
424424

425425
init_ghc_hs_iface();
426426

427-
/* Must match the definition's #if at line 108: wasm32 is excluded there
428-
* (promoteBootLibrariesToGlobal uses dladdr/dlopen, unavailable on wasm),
429-
* but wasm DOES define RTLD_NOLOAD, so without the wasm32 exclusion here the
430-
* call fires against an undefined function ("call to undeclared function
431-
* 'promoteBootLibrariesToGlobal'"). */
432427
#if !defined(mingw32_HOST_OS) && !defined(wasm32_HOST_ARCH) && defined(RTLD_NOLOAD)
433428
/* Promote boot libraries to RTLD_GLOBAL for dynamic code loading.
434429
* See Note [Promoting Boot Libraries to RTLD_GLOBAL] */
430+
/* NOTE: matches the function definition's #if at line 108 — wasm32 needs
431+
* to be excluded here too (the function is undefined for wasm). */
435432
promoteBootLibrariesToGlobal();
436433
#endif
437434

rts/linker/elf_got.c

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,51 @@
55
#include <string.h>
66

77
#if defined(OBJFORMAT_ELF)
8+
9+
/*
10+
* Check whether a symbol (identified by its symbol table section index and
11+
* its index within that table) is referenced by any relocation whose type
12+
* is not R_*_NONE (type 0). R_*_NONE is a no-op relocation for every ELF
13+
* architecture, so symbols referenced exclusively by NONE relocations need
14+
* not be resolved.
15+
*
16+
* This is used by fillGot() to avoid hard failures on undefined symbols that
17+
* the assembler emitted NONE relocations for (e.g. zero-length arrays
18+
* optimised away by the compiler but still present in the symbol table).
19+
*/
20+
static bool
21+
symbolHasNonNoneRelocation(ObjectCode *oc, unsigned symTabIndex,
22+
size_t symIdx)
23+
{
24+
/* Scan REL relocation tables */
25+
for (ElfRelocationTable *relTab = oc->info->relTable;
26+
relTab != NULL; relTab = relTab->next) {
27+
if (relTab->sectionHeader->sh_link == symTabIndex) {
28+
for (size_t i = 0; i < relTab->n_relocations; i++) {
29+
Elf_Rel *rel = &relTab->relocations[i];
30+
if (ELF_R_SYM(rel->r_info) == symIdx
31+
&& ELF_R_TYPE(rel->r_info) != 0) {
32+
return true;
33+
}
34+
}
35+
}
36+
}
37+
/* Scan RELA relocation tables */
38+
for (ElfRelocationATable *relaTab = oc->info->relaTable;
39+
relaTab != NULL; relaTab = relaTab->next) {
40+
if (relaTab->sectionHeader->sh_link == symTabIndex) {
41+
for (size_t i = 0; i < relaTab->n_relocations; i++) {
42+
Elf_Rela *rel = &relaTab->relocations[i];
43+
if (ELF_R_SYM(rel->r_info) == symIdx
44+
&& ELF_R_TYPE(rel->r_info) != 0) {
45+
return true;
46+
}
47+
}
48+
}
49+
}
50+
return false;
51+
}
52+
853
/*
954
* Check if we need a global offset table slot for a
1055
* given symbol
@@ -96,6 +141,21 @@ fillGot(ObjectCode * oc) {
96141
if(0x0 == symbol->addr) {
97142
if(0 == strncmp(symbol->name,"_GLOBAL_OFFSET_TABLE_",21)) {
98143
symbol->addr = oc->info->got_start;
144+
} else if (!symbolHasNonNoneRelocation(oc, symTab->index, i)) {
145+
/* Symbol is only referenced by R_*_NONE
146+
* (type 0) relocations, which are no-ops.
147+
* This can happen when e.g. a zero-length
148+
* array is optimised away by the compiler
149+
* but the assembler still emits NONE relocs
150+
* against the (now undefined) symbol.
151+
* Assign a dummy address so the GOT slot is
152+
* filled; it will never be dereferenced. */
153+
IF_DEBUG(linker,
154+
debugBelch("Skipping unresolvable symbol"
155+
" '%s' (only referenced by"
156+
" NONE relocations)\n",
157+
symbol->name));
158+
symbol->addr = (void*)0xDEAD0000;
99159
} else {
100160
errorBelch("Failed to lookup symbol: %s,"
101161
" you might consider using --optimistic-linking\n",

0 commit comments

Comments
 (0)