Browse Source

feat: copy frameworks to Frameworks folder, detect linkers, custom target dirs (#4174)

* feat: copy frameworks to Frameworks folder

* add dylib linking to patching

* remove logging

* rename __aslr_offset

* fix android/linux

* support linker flavor

* support custom target dirs

* custom cargo target dir support

* whoops ios

* dlsym doesn't exist on windows

* look for the right main symbol per platform

* distinction between mains

* export main on windows

* enbable rlib caching

* add dx hash

* adjust linking warnings

* update tui with hotreload state

* implement symbol flags

* add flags

* only run ranlib on darwin

* enable lld

* paths

* add harness test for tls

* wip: revert launch mobile

* wip

* fix fullstack mobile

* fix a few bugs
- long paths?
- reverting back to original

* pass-thru fuse-ld args

* fix nan in render, fix empty archives

* consider existing dep files if archive exists

* fmt

* drop msrv
Jonathan Kelley 1 month ago
parent
commit
0982c1ff8b

+ 30 - 0
Cargo.lock

@@ -2022,6 +2022,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "cargo-config2"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dc3749a36e0423c991f1e7a3e4ab0c36a1f489658313db4b187d401d79cc461"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "toml_edit 0.22.26",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "cargo-generate"
 version = "0.23.3"
@@ -2943,6 +2955,14 @@ dependencies = [
  "itertools 0.10.5",
 ]
 
+[[package]]
+name = "cross-tls-crate"
+version = "0.1.0"
+
+[[package]]
+name = "cross-tls-crate-dylib"
+version = "0.1.0"
+
 [[package]]
 name = "crossbeam"
 version = "0.8.4"
@@ -3662,6 +3682,7 @@ dependencies = [
  "backtrace",
  "brotli 7.0.0",
  "built",
+ "cargo-config2",
  "cargo-generate",
  "cargo_metadata",
  "cargo_toml",
@@ -13468,6 +13489,15 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "subsecond-tls-harness"
+version = "0.1.0"
+dependencies = [
+ "cross-tls-crate",
+ "cross-tls-crate-dylib",
+ "dioxus-devtools",
+]
+
 [[package]]
 name = "subsecond-types"
 version = "0.7.0-alpha.0"

+ 3 - 0
Cargo.toml

@@ -94,6 +94,9 @@ members = [
     # subsecond
     "packages/subsecond/subsecond",
     "packages/subsecond/subsecond-types",
+    "packages/subsecond/subsecond-tests/cross-tls-crate",
+    "packages/subsecond/subsecond-tests/cross-tls-crate-dylib",
+    "packages/subsecond/subsecond-tests/cross-tls-test",
 
     # Full project examples
     "example-projects/fullstack-hackernews",

+ 1 - 0
packages/cli/Cargo.toml

@@ -138,6 +138,7 @@ wasm-bindgen-externref-xform = "0.2.100"
 pdb = "0.8.0"
 self_update = { version = "0.42.0", features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate"] }
 self-replace = "1.5.0"
+cargo-config2 = { workspace = true }
 
 [build-dependencies]
 built = { version = "0.7.5", features = ["git2"] }

+ 2 - 2
packages/cli/src/build/builder.rs

@@ -208,12 +208,12 @@ impl AppBuilder {
                             self.bundling_progress = 0.0;
                         }
                         BuildStage::Starting { crate_count, .. } => {
-                            self.expected_crates = *crate_count;
+                            self.expected_crates = *crate_count.max(&1);
                         }
                         BuildStage::InstallingTooling => {}
                         BuildStage::Compiling { current, total, .. } => {
                             self.compiled_crates = *current;
-                            self.expected_crates = *total;
+                            self.expected_crates = *total.max(&1);
 
                             if self.compile_start.is_none() {
                                 self.compile_start = Some(Instant::now());

+ 70 - 40
packages/cli/src/build/patch.rs

@@ -3,8 +3,8 @@ use itertools::Itertools;
 use object::{
     macho::{self},
     read::File,
-    write::{MachOBuildVersion, StandardSection, Symbol, SymbolSection},
-    Endianness, Object, ObjectSymbol, SymbolKind, SymbolScope,
+    write::{MachOBuildVersion, SectionId, StandardSection, Symbol, SymbolId, SymbolSection},
+    Endianness, Object, ObjectSymbol, SymbolFlags, SymbolKind, SymbolScope,
 };
 use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
 use std::{
@@ -84,6 +84,7 @@ pub struct CachedSymbol {
     pub is_undefined: bool,
     pub is_weak: bool,
     pub size: u64,
+    pub flags: SymbolFlags<SectionId, SymbolId>,
 }
 
 impl PartialEq for HotpatchModuleCache {
@@ -139,6 +140,7 @@ impl HotpatchModuleCache {
                                     is_undefined,
                                     is_weak: false,
                                     size: 0,
+                                    flags: SymbolFlags::None,
                                 },
                             );
                         }
@@ -158,6 +160,7 @@ impl HotpatchModuleCache {
                                     is_undefined,
                                     is_weak: false,
                                     size: 0,
+                                    flags: SymbolFlags::None,
                                 },
                             );
                         }
@@ -252,6 +255,15 @@ impl HotpatchModuleCache {
                 let symbol_table = obj
                     .symbols()
                     .filter_map(|s| {
+                        let flags = match s.flags() {
+                            SymbolFlags::None => SymbolFlags::None,
+                            SymbolFlags::Elf { st_info, st_other } => {
+                                SymbolFlags::Elf { st_info, st_other }
+                            }
+                            SymbolFlags::MachO { n_desc } => SymbolFlags::MachO { n_desc },
+                            _ => SymbolFlags::None,
+                        };
+
                         Some((
                             s.name().ok()?.to_string(),
                             CachedSymbol {
@@ -260,6 +272,7 @@ impl HotpatchModuleCache {
                                 is_weak: s.is_weak(),
                                 kind: s.kind(),
                                 size: s.size(),
+                                flags,
                             },
                         ))
                     })
@@ -326,9 +339,9 @@ fn create_windows_jump_table(patch: &Path, cache: &HotpatchModuleCache) -> Resul
         .context("failed to find 'main' symbol in patch")?;
 
     let aslr_reference = old_name_to_addr
-        .get("__aslr_reference")
+        .get("main")
         .map(|s| s.address)
-        .context("failed to find '_aslr_reference' symbol in original module")?;
+        .context("failed to find '_main' symbol in original module")?;
 
     Ok(JumpTable {
         lib: patch.to_path_buf(),
@@ -371,31 +384,15 @@ fn create_native_jump_table(
         }
     }
 
-    let new_base_address = match triple.operating_system {
-        // The symbol in the symtab is called "_main" but in the dysymtab it is called "main"
-        OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
-            *new_name_to_addr
-                .get("_main")
-                .context("failed to find '_main' symbol in patch")?
-        }
-
-        // No distincation between the two on these platforms
-        OperatingSystem::Freebsd
-        | OperatingSystem::Openbsd
-        | OperatingSystem::Linux
-        | OperatingSystem::Windows => *new_name_to_addr
-            .get("main")
-            .context("failed to find 'main' symbol in patch")?,
-
-        // On wasm, it doesn't matter what the address is since the binary is PIC
-        _ => 0,
-    };
-
+    let sentinel = main_sentinel(triple);
+    let new_base_address = new_name_to_addr
+        .get(sentinel)
+        .cloned()
+        .context("failed to find 'main' symbol in base - are deubg symbols enabled?")?;
     let aslr_reference = old_name_to_addr
-        .get("___aslr_reference")
-        .or_else(|| old_name_to_addr.get("__aslr_reference"))
+        .get(sentinel)
         .map(|s| s.address)
-        .context("failed to find '___aslr_reference' symbol in original module")?;
+        .context("failed to find 'main' symbol in original module - are debug symbols enabled?")?;
 
     Ok(JumpTable {
         lib: patch.to_path_buf(),
@@ -831,22 +828,31 @@ pub fn create_undefined_symbol_stub(
         _ => {}
     }
 
-    let symbol_table = &cache.symbol_table;
+    // Get the offset from the main module and adjust the addresses by the slide;
+    let aslr_ref_address = cache
+        .symbol_table
+        .get(main_sentinel(triple))
+        .context("failed to find '_main' symbol in patch")?
+        .address;
+
+    if aslr_reference < aslr_ref_address {
+        return Err(PatchError::InvalidModule(
+            format!(
+            "ASLR reference is less than the main module's address - is there a `main`?. {:x} < {:x}", aslr_reference, aslr_ref_address )
+        ));
+    }
 
-    // Get the offset from the main module and adjust the addresses by the slide
-    let aslr_ref_address = symbol_table
-        .get("___aslr_reference")
-        .or_else(|| symbol_table.get("__aslr_reference"))
-        .map(|s| s.address)
-        .context("Failed to find ___aslr_reference symbol")?;
     let aslr_offset = aslr_reference - aslr_ref_address;
 
     // we need to assemble a PLT/GOT so direct calls to the patch symbols work
     // for each symbol we either write the address directly (as a symbol) or create a PLT/GOT entry
     let text_section = obj.section_id(StandardSection::Text);
     for name in undefined_symbols {
-        let Some(sym) = symbol_table.get(name.as_str().trim_start_matches("__imp_")) else {
-            tracing::error!("Symbol not found: {}", name);
+        let Some(sym) = cache
+            .symbol_table
+            .get(name.as_str().trim_start_matches("__imp_"))
+        else {
+            tracing::debug!("Symbol not found: {}", name);
             continue;
         };
 
@@ -899,7 +905,7 @@ pub fn create_undefined_symbol_stub(
                     kind: SymbolKind::Data, // Always Data for IAT entries
                     weak: false,
                     section: SymbolSection::Section(data_section),
-                    flags: object::SymbolFlags::None,
+                    flags: SymbolFlags::None,
                 });
             }
 
@@ -1028,7 +1034,7 @@ pub fn create_undefined_symbol_stub(
                     kind: SymbolKind::Text,
                     weak: false,
                     section: SymbolSection::Section(text_section),
-                    flags: object::SymbolFlags::None,
+                    flags: SymbolFlags::None, // ignore for these stubs
                 });
             }
 
@@ -1097,7 +1103,7 @@ pub fn create_undefined_symbol_stub(
                     kind: SymbolKind::Tls,
                     weak: false,
                     section: SymbolSection::Section(tls_section),
-                    flags: object::SymbolFlags::None,
+                    flags: SymbolFlags::None, // ignore for these stubs
                 });
             }
 
@@ -1108,6 +1114,15 @@ pub fn create_undefined_symbol_stub(
                     SymbolKind::Unknown => SymbolKind::Data,
                     k => k,
                 };
+
+                // plain linux *wants* these flags, but android doesn't.
+                // unsure what's going on here, but this is special cased for now.
+                // I think the more advanced linkers don't want these flags, but the default linux linker (ld) does.
+                let flags = match triple.environment {
+                    target_lexicon::Environment::Android => SymbolFlags::None,
+                    _ => sym.flags,
+                };
+
                 obj.add_symbol(Symbol {
                     name: name.as_bytes()[name_offset..].to_vec(),
                     value: abs_addr,
@@ -1116,7 +1131,7 @@ pub fn create_undefined_symbol_stub(
                     kind,
                     weak: sym.is_weak,
                     section: SymbolSection::Absolute,
-                    flags: object::SymbolFlags::None,
+                    flags,
                 });
             }
         }
@@ -1439,3 +1454,18 @@ fn parse_module_with_ids(bindgened: &[u8]) -> Result<ParsedModule> {
         symbols,
     })
 }
+
+/// Get the main sentinel symbol for the given target triple
+///
+/// We need to special case darwin since `main` is the entrypoint but `_main` is the actual symbol.
+/// The entrypoint ends up outside the text section, seemingly, and breaks our aslr detection.
+fn main_sentinel(triple: &Triple) -> &'static str {
+    match triple.operating_system {
+        // The symbol in the symtab is called "_main" but in the dysymtab it is called "main"
+        OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
+            "_main"
+        }
+
+        _ => "main",
+    }
+}

+ 319 - 115
packages/cli/src/build/request.rs

@@ -316,8 +316,9 @@
 //! - xbuild: <https://github.com/rust-mobile/xbuild/blob/master/xbuild/src/command/build.rs>
 
 use crate::{
-    AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, Platform, Result, RustcArgs,
-    TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace, DX_RUSTC_WRAPPER_ENV_VAR,
+    AndroidTools, BuildContext, DioxusConfig, Error, LinkAction, LinkerFlavor, Platform, Result,
+    RustcArgs, TargetArgs, TraceSrc, WasmBindgen, WasmOptConfig, Workspace,
+    DX_RUSTC_WRAPPER_ENV_VAR,
 };
 use anyhow::Context;
 use cargo_metadata::diagnostic::Diagnostic;
@@ -377,7 +378,7 @@ pub(crate) struct BuildRequest {
     pub(crate) extra_cargo_args: Vec<String>,
     pub(crate) extra_rustc_args: Vec<String>,
     pub(crate) no_default_features: bool,
-    pub(crate) custom_target_dir: Option<PathBuf>,
+    pub(crate) target_dir: PathBuf,
     pub(crate) skip_assets: bool,
     pub(crate) wasm_split: bool,
     pub(crate) debug_symbols: bool,
@@ -643,12 +644,26 @@ impl BuildRequest {
             },
         };
 
-        let custom_linker = if platform == Platform::Android {
+        // Somethings we override are also present in the user's config.
+        // If we can't get them by introspecting cargo, then we need to get them from the config
+        let cargo_config = cargo_config2::Config::load().unwrap();
+
+        let mut custom_linker = if platform == Platform::Android {
             Some(workspace.android_tools()?.android_cc(&triple))
         } else {
             None
         };
 
+        if let Ok(Some(linker)) = cargo_config.linker(triple.to_string()) {
+            custom_linker = Some(linker);
+        }
+
+        let target_dir = std::env::var("CARGO_TARGET_DIR")
+            .ok()
+            .map(PathBuf::from)
+            .or_else(|| cargo_config.build.target_dir.clone())
+            .unwrap_or_else(|| workspace.workspace_root().join("target"));
+
         // Set up some tempfiles so we can do some IPC between us and the linker/rustc wrapper (which is occasionally us!)
         let link_args_file = Arc::new(
             NamedTempFile::with_suffix(".txt")
@@ -677,11 +692,16 @@ impl BuildRequest {
                 • link_args_file: {},
                 • link_err_file: {},
                 • rustc_wrapper_args_file: {},
-                • session_cache_dir: {}"#,
+                • session_cache_dir: {}
+                • linker: {:?}
+                • target_dir: {:?}
+                "#,
             link_args_file.path().display(),
             link_err_file.path().display(),
             rustc_wrapper_args_file.path().display(),
             session_cache_dir.path().display(),
+            custom_linker,
+            target_dir,
         );
 
         Ok(Self {
@@ -696,7 +716,7 @@ impl BuildRequest {
             workspace,
             config,
             enabled_platforms,
-            custom_target_dir: None,
+            target_dir,
             custom_linker,
             link_args_file,
             link_err_file,
@@ -739,6 +759,9 @@ impl BuildRequest {
                 self.write_executable(ctx, &artifacts.exe, &mut artifacts.assets)
                     .await
                     .context("Failed to write main executable")?;
+                self.write_frameworks(ctx, &artifacts.direct_rustc)
+                    .await
+                    .context("Failed to write frameworks")?;
                 self.write_assets(ctx, &artifacts.assets)
                     .await
                     .context("Failed to write assets")?;
@@ -874,8 +897,6 @@ impl BuildRequest {
             }
         }
 
-        let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
-
         // Accumulate the rustc args from the wrapper, if they exist and can be parsed.
         let mut direct_rustc = RustcArgs::default();
         if let Ok(res) = std::fs::read_to_string(self.rustc_wrapper_args_file.path()) {
@@ -887,13 +908,34 @@ impl BuildRequest {
         // If there's any warnings from the linker, we should print them out
         if let Ok(linker_warnings) = std::fs::read_to_string(self.link_err_file.path()) {
             if !linker_warnings.is_empty() {
-                tracing::warn!("Linker warnings: {}", linker_warnings);
+                if output_location.is_none() {
+                    tracing::error!("Linker warnings: {}", linker_warnings);
+                } else {
+                    tracing::debug!("Linker warnings: {}", linker_warnings);
+                }
             }
         }
 
+        // Collect the linker args from the and update the rustc args
+        direct_rustc.link_args = std::fs::read_to_string(self.link_args_file.path())
+            .context("Failed to read link args from file")?
+            .lines()
+            .map(|s| s.to_string())
+            .collect::<Vec<_>>();
+
+        let exe = output_location.context("Cargo build failed - no output location. Toggle tracing mode (press `t`) for more information.")?;
+
         // Fat builds need to be linked with the fat linker. Would also like to link here for thin builds
         if matches!(ctx.mode, BuildMode::Fat) {
+            let link_start = SystemTime::now();
             self.run_fat_link(ctx, &exe, &direct_rustc).await?;
+            tracing::debug!(
+                "Fat linking completed in {}us",
+                SystemTime::now()
+                    .duration_since(link_start)
+                    .unwrap()
+                    .as_micros()
+            );
         }
 
         let assets = self.collect_assets(&exe, ctx)?;
@@ -901,7 +943,11 @@ impl BuildRequest {
         let mode = ctx.mode.clone();
         let platform = self.platform;
 
-        tracing::debug!("Build completed successfully: {:?}", exe);
+        tracing::debug!(
+            "Build completed successfully in {}us: {:?}",
+            time_end.duration_since(time_start).unwrap().as_micros(),
+            exe
+        );
 
         Ok(BuildArtifacts {
             time_end,
@@ -991,12 +1037,6 @@ impl BuildRequest {
             | Platform::Ios
             | Platform::Liveview
             | Platform::Server => {
-                // We wipe away the dir completely, which is not great behavior :/
-                // Don't wipe server since web will need this folder too.
-                if self.platform != Platform::Server {
-                    _ = std::fs::remove_dir_all(self.exe_dir());
-                }
-
                 std::fs::create_dir_all(self.exe_dir())?;
                 std::fs::copy(exe, self.main_exe())?;
             }
@@ -1005,6 +1045,52 @@ impl BuildRequest {
         Ok(())
     }
 
+    async fn write_frameworks(&self, _ctx: &BuildContext, direct_rustc: &RustcArgs) -> Result<()> {
+        let framework_dir = self.frameworks_folder();
+        _ = std::fs::create_dir_all(&framework_dir);
+
+        for arg in &direct_rustc.link_args {
+            // todo - how do we handle windows dlls? we don't want to bundle the system dlls
+            // for now, we don't do anything with dlls, and only use .dylibs and .so files
+            if arg.ends_with(".dylib") | arg.ends_with(".so") {
+                let from = PathBuf::from(arg);
+                let to = framework_dir.join(from.file_name().unwrap());
+                _ = std::fs::remove_file(&to);
+
+                tracing::debug!("Copying framework from {from:?} to {to:?}");
+
+                // in dev and on normal oses, we want to symlink the file
+                // otherwise, just copy it (since in release you want to distribute the framework)
+                if cfg!(any(windows, unix)) && !self.release {
+                    #[cfg(windows)]
+                    std::os::windows::fs::symlink_file(from, to).with_context(|| {
+                        "Failed to symlink framework into bundle: {from:?} -> {to:?}"
+                    })?;
+
+                    #[cfg(unix)]
+                    std::os::unix::fs::symlink(from, to).with_context(|| {
+                        "Failed to symlink framework into bundle: {from:?} -> {to:?}"
+                    })?;
+                } else {
+                    std::fs::copy(from, to)?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn frameworks_folder(&self) -> PathBuf {
+        match self.triple.operating_system {
+            OperatingSystem::Darwin(_) | OperatingSystem::MacOSX(_) => {
+                self.root_dir().join("Contents").join("Frameworks")
+            }
+            OperatingSystem::IOS(_) => self.root_dir().join("Frameworks"),
+            OperatingSystem::Linux | OperatingSystem::Windows => self.root_dir(),
+            _ => self.root_dir(),
+        }
+    }
+
     /// Copy the assets out of the manifest and into the target location
     ///
     /// Should be the same on all platforms - just copy over the assets from the manifest into the output directory
@@ -1200,6 +1286,7 @@ impl BuildRequest {
         //     -nodefaultlibs
         //     -Wl,-all_load
         // ```
+        let mut dylibs = vec![];
         let mut object_files = args
             .iter()
             .filter(|arg| arg.ends_with(".rcgu.o"))
@@ -1233,6 +1320,15 @@ impl BuildRequest {
             let patch_file = self.main_exe().with_file_name("stub.o");
             std::fs::write(&patch_file, stub_bytes)?;
             object_files.push(patch_file);
+
+            // Add the dylibs/sos to the linker args
+            // Make sure to use the one in the bundle, not the ones in the target dir or system.
+            for arg in &rustc_args.link_args {
+                if arg.ends_with(".dylib") || arg.ends_with(".so") {
+                    let path = PathBuf::from(arg);
+                    dylibs.push(self.frameworks_folder().join(path.file_name().unwrap()));
+                }
+            }
         }
 
         // And now we can run the linker with our new args
@@ -1251,6 +1347,7 @@ impl BuildRequest {
         // does it since it uses llvm-objcopy into the `target/debug/` folder.
         let res = Command::new(linker)
             .args(object_files.iter())
+            .args(dylibs.iter())
             .args(self.thin_link_args(&args)?)
             .args(out_arg)
             .env_clear()
@@ -1298,12 +1395,9 @@ impl BuildRequest {
     /// This is basically just stripping away the rlibs and other libraries that will be satisfied
     /// by our stub step.
     fn thin_link_args(&self, original_args: &[&str]) -> Result<Vec<String>> {
-        use target_lexicon::OperatingSystem;
-
-        let triple = self.triple.clone();
         let mut out_args = vec![];
 
-        match triple.operating_system {
+        match self.linker_flavor() {
             // wasm32-unknown-unknown -> use wasm-ld (gnu-lld)
             //
             // We need to import a few things - namely the memory and ifunc table.
@@ -1323,7 +1417,7 @@ impl BuildRequest {
             // I think we can make relocation-model=pic work for non-wasm platforms, enabling
             // fully relocatable modules with no host coordination in lieu of sending out
             // the aslr slide at runtime.
-            OperatingSystem::Unknown if self.platform == Platform::Web => {
+            LinkerFlavor::WasmLld => {
                 out_args.extend([
                     "--fatal-warnings".to_string(),
                     "--verbose".to_string(),
@@ -1344,7 +1438,7 @@ impl BuildRequest {
             //
             // Most importantly, we want to pass `-dylib` to both CC and the linker to indicate that
             // we want to generate the shared library instead of an executable.
-            OperatingSystem::IOS(_) | OperatingSystem::MacOSX(_) | OperatingSystem::Darwin(_) => {
+            LinkerFlavor::Darwin => {
                 out_args.extend(["-Wl,-dylib".to_string()]);
 
                 // Preserve the original args. We only preserve:
@@ -1367,7 +1461,7 @@ impl BuildRequest {
             // android/linux need to be compatible with lld
             //
             // android currently drags along its own libraries and other zany flags
-            OperatingSystem::Linux => {
+            LinkerFlavor::Gnu => {
                 out_args.extend([
                     "-shared".to_string(),
                     "-Wl,--eh-frame-hdr".to_string(),
@@ -1391,13 +1485,14 @@ impl BuildRequest {
                     if arg.starts_with("-l")
                         || arg.starts_with("-m")
                         || arg.starts_with("-Wl,--target=")
+                        || arg.starts_with("-Wl,-fuse-ld")
                     {
                         out_args.push(arg.to_string());
                     }
                 }
             }
 
-            OperatingSystem::Windows => {
+            LinkerFlavor::Msvc => {
                 out_args.extend([
                     "shlwapi.lib".to_string(),
                     "kernel32.lib".to_string(),
@@ -1415,7 +1510,9 @@ impl BuildRequest {
                 ]);
             }
 
-            _ => return Err(anyhow::anyhow!("Unsupported platform for thin linking").into()),
+            LinkerFlavor::Unsupported => {
+                return Err(anyhow::anyhow!("Unsupported platform for thin linking").into())
+            }
         }
 
         let extract_value = |arg: &str| -> Option<String> {
@@ -1460,15 +1557,12 @@ impl BuildRequest {
                 .unwrap_or(0),
         ));
 
-        let extension = match self.triple.operating_system {
-            OperatingSystem::Darwin(_) => "dylib",
-            OperatingSystem::MacOSX(_) => "dylib",
-            OperatingSystem::IOS(_) => "dylib",
-            OperatingSystem::Windows => "dll",
-            OperatingSystem::Linux => "so",
-            OperatingSystem::Wasi => "wasm",
-            OperatingSystem::Unknown if self.platform == Platform::Web => "wasm",
-            _ => "",
+        let extension = match self.linker_flavor() {
+            LinkerFlavor::Darwin => "dylib",
+            LinkerFlavor::Gnu => "so",
+            LinkerFlavor::WasmLld => "wasm",
+            LinkerFlavor::Msvc => "dll",
+            LinkerFlavor::Unsupported => "",
         };
 
         path.with_extension(extension)
@@ -1512,38 +1606,65 @@ impl BuildRequest {
     ) -> Result<()> {
         ctx.status_starting_link();
 
-        let raw_args = std::fs::read_to_string(self.link_args_file.path())
-            .context("Failed to read link args from file")?;
-        let args = raw_args.lines().collect::<Vec<_>>();
-
         // Filter out the rlib files from the arguments
-        let rlibs = args
+        let rlibs = rustc_args
+            .link_args
             .iter()
             .filter(|arg| arg.ends_with(".rlib"))
             .map(PathBuf::from)
             .collect::<Vec<_>>();
 
-        // Acquire a hash from the rlib names
+        // Acquire a hash from the rlib names, sizes, modified times, and dx's git commit hash
+        // This ensures that any changes in dx or the rlibs will cause a new hash to be generated
+        // The hash relies on both dx and rustc hashes, so it should be thoroughly unique. Keep it
+        // short to avoid long file names.
         let hash_id = Uuid::new_v5(
             &Uuid::NAMESPACE_OID,
             rlibs
                 .iter()
-                .map(|p| p.file_name().unwrap().to_string_lossy())
+                .map(|p| {
+                    format!(
+                        "{}-{}-{}-{}",
+                        p.file_name().unwrap().to_string_lossy(),
+                        p.metadata().map(|m| m.len()).unwrap_or_default(),
+                        p.metadata()
+                            .ok()
+                            .and_then(|m| m.modified().ok())
+                            .and_then(|f| f.duration_since(UNIX_EPOCH).map(|f| f.as_secs()).ok())
+                            .unwrap_or_default(),
+                        crate::dx_build_info::GIT_COMMIT_HASH.unwrap_or_default()
+                    )
+                })
                 .collect::<String>()
                 .as_bytes(),
-        );
+        )
+        .to_string()
+        .chars()
+        .take(8)
+        .collect::<String>();
 
         // Check if we already have a cached object file
-        let out_ar_path = exe.with_file_name(format!("libfatdependencies-{hash_id}.a"));
+        let out_ar_path = exe.with_file_name(format!("libdeps-{hash_id}.a",));
+        let out_rlibs_list = exe.with_file_name(format!("rlibs-{hash_id}.txt"));
+        let mut archive_has_contents = out_ar_path.exists();
 
-        let mut compiler_rlibs = vec![];
+        // Use the rlibs list if it exists
+        let mut compiler_rlibs = std::fs::read_to_string(&out_rlibs_list)
+            .ok()
+            .map(|s| s.lines().map(PathBuf::from).collect::<Vec<_>>())
+            .unwrap_or_default();
 
         // Create it by dumping all the rlibs into it
         // This will include the std rlibs too, which can severely bloat the size of the archive
         //
         // The nature of this process involves making extremely fat archives, so we should try and
         // speed up the future linking process by caching the archive.
-        if !crate::devcfg::should_cache_dep_lib(&out_ar_path) {
+        //
+        // Since we're using the git hash for the CLI entropy, debug builds should always regenerate
+        // the archive since their hash might not change, but the logic might.
+        if !archive_has_contents || cfg!(debug_assertions) {
+            compiler_rlibs.clear();
+
             let mut bytes = vec![];
             let mut out_ar = ar::Builder::new(&mut bytes);
             for rlib in &rlibs {
@@ -1573,7 +1694,11 @@ impl BuildRequest {
                     }
 
                     // rlibs might contain dlls/sos/lib files which we don't want to include
-                    if name.ends_with(".dll") || name.ends_with(".so") || name.ends_with(".lib") {
+                    if name.ends_with(".dll")
+                        || name.ends_with(".so")
+                        || name.ends_with(".lib")
+                        || name.ends_with(".dylib")
+                    {
                         compiler_rlibs.push(rlib.to_owned());
                         continue;
                     }
@@ -1582,6 +1707,7 @@ impl BuildRequest {
                         tracing::debug!("Unknown object file in rlib: {:?}", name);
                     }
 
+                    archive_has_contents = true;
                     out_ar
                         .append(&object_file.header().clone(), object_file)
                         .context("Failed to add object file to archive")?;
@@ -1595,8 +1721,10 @@ impl BuildRequest {
             // Run the ranlib command to index the archive. This slows down this process a bit,
             // but is necessary for some linkers to work properly.
             // We ignore its error in case it doesn't recognize the architecture
-            if let Some(ranlib) = self.select_ranlib() {
-                _ = Command::new(ranlib).arg(&out_ar_path).output().await;
+            if self.linker_flavor() == LinkerFlavor::Darwin {
+                if let Some(ranlib) = self.select_ranlib() {
+                    _ = Command::new(ranlib).arg(&out_ar_path).output().await;
+                }
             }
         }
 
@@ -1606,62 +1734,71 @@ impl BuildRequest {
         // And then remove the rest of the rlibs
         //
         // We also need to insert the -force_load flag to force the linker to load the archive
-        let mut args = args.iter().map(|s| s.to_string()).collect::<Vec<_>>();
-        if let Some(first_rlib) = args.iter().position(|arg| arg.ends_with(".rlib")) {
-            match self.triple.operating_system {
-                OperatingSystem::Unknown if self.platform == Platform::Web => {
-                    // We need to use the --whole-archive flag for wasm
-                    args[first_rlib] = "--whole-archive".to_string();
-                    args.insert(first_rlib + 1, out_ar_path.display().to_string());
-                    args.insert(first_rlib + 2, "--no-whole-archive".to_string());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 3, rlib.display().to_string());
+        let mut args = rustc_args.link_args.clone();
+        if let Some(last_object) = args.iter().rposition(|arg| arg.ends_with(".o")) {
+            if archive_has_contents {
+                match self.linker_flavor() {
+                    LinkerFlavor::WasmLld => {
+                        args.insert(last_object, "--whole-archive".to_string());
+                        args.insert(last_object + 1, out_ar_path.display().to_string());
+                        args.insert(last_object + 2, "--no-whole-archive".to_string());
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 3, rlib.display().to_string());
+                        }
                     }
-                }
-
-                // Subtle difference - on linux and android we go through clang and thus pass `-Wl,` prefix
-                OperatingSystem::Linux => {
-                    args[first_rlib] = "-Wl,--whole-archive".to_string();
-                    args.insert(first_rlib + 1, out_ar_path.display().to_string());
-                    args.insert(first_rlib + 2, "-Wl,--no-whole-archive".to_string());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 3, rlib.display().to_string());
+                    LinkerFlavor::Gnu => {
+                        args.insert(last_object, "-Wl,--whole-archive".to_string());
+                        args.insert(last_object + 1, out_ar_path.display().to_string());
+                        args.insert(last_object + 2, "-Wl,--no-whole-archive".to_string());
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 3, rlib.display().to_string());
+                        }
                     }
-                }
-
-                OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
-                    args[first_rlib] = "-Wl,-force_load".to_string();
-                    args.insert(first_rlib + 1, out_ar_path.display().to_string());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 2, rlib.display().to_string());
+                    LinkerFlavor::Darwin => {
+                        args.insert(last_object, "-Wl,-force_load".to_string());
+                        args.insert(last_object + 1, out_ar_path.display().to_string());
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 2, rlib.display().to_string());
+                        }
                     }
-
-                    args.insert(first_rlib + 3, "-Wl,-all_load".to_string());
-                }
-
-                OperatingSystem::Windows => {
-                    args[first_rlib] = format!("/WHOLEARCHIVE:{}", out_ar_path.display());
-                    args.retain(|arg| !arg.ends_with(".rlib"));
-
-                    // add back the compiler rlibs
-                    for rlib in compiler_rlibs.iter().rev() {
-                        args.insert(first_rlib + 1, rlib.display().to_string());
+                    LinkerFlavor::Msvc => {
+                        args.insert(
+                            last_object,
+                            format!("/WHOLEARCHIVE:{}", out_ar_path.display()),
+                        );
+                        args.retain(|arg| !arg.ends_with(".rlib"));
+                        for rlib in compiler_rlibs.iter().rev() {
+                            args.insert(last_object + 1, rlib.display().to_string());
+                        }
+                    }
+                    LinkerFlavor::Unsupported => {
+                        tracing::error!("Unsupported platform for fat linking");
                     }
+                };
+            }
+        }
 
-                    args.insert(first_rlib, "/HIGHENTROPYVA:NO".to_string());
-                }
+        // Add custom args to the linkers
+        match self.linker_flavor() {
+            LinkerFlavor::Gnu => {
+                // Export `main` so subsecond can use it for a reference point
+                args.push("-Wl,--export-dynamic-symbol,main".to_string());
+            }
+            LinkerFlavor::Darwin => {
+                // `-all_load` is an extra step to ensure that all symbols are loaded (different than force_load)
+                args.push("-Wl,-all_load".to_string());
+            }
+            LinkerFlavor::Msvc => {
+                // Prevent alsr from overflowing 32 bits
+                args.push("/HIGHENTROPYVA:NO".to_string());
 
-                _ => {}
-            };
+                // Export `main` so subsecond can use it for a reference point
+                args.push("/EXPORT:main".to_string());
+            }
+            LinkerFlavor::WasmLld | LinkerFlavor::Unsupported => {}
         }
 
         // We also need to remove the `-o` flag since we want the linker output to end up in the
@@ -1690,6 +1827,7 @@ impl BuildRequest {
         let linker = self.select_linker()?;
 
         tracing::trace!("Fat linking with args: {:?} {:#?}", linker, args);
+        tracing::trace!("Fat linking with env: {:#?}", rustc_args.envs);
 
         // Run the linker directly!
         let out_arg = match self.triple.operating_system {
@@ -1724,9 +1862,64 @@ impl BuildRequest {
             _ = std::fs::remove_file(f);
         }
 
+        // Cache the rlibs list
+        _ = std::fs::write(
+            &out_rlibs_list,
+            compiler_rlibs
+                .into_iter()
+                .map(|s| s.display().to_string())
+                .join("\n"),
+        );
+
         Ok(())
     }
 
+    /// Automatically detect the linker flavor based on the target triple and any custom linkers.
+    ///
+    /// This tries to replicate what rustc does when selecting the linker flavor based on the linker
+    /// and triple.
+    fn linker_flavor(&self) -> LinkerFlavor {
+        if let Some(custom) = self.custom_linker.as_ref() {
+            let name = custom.file_name().unwrap().to_ascii_lowercase();
+            match name.to_str() {
+                Some("lld-link") => return LinkerFlavor::Msvc,
+                Some("lld-link.exe") => return LinkerFlavor::Msvc,
+                Some("wasm-ld") => return LinkerFlavor::WasmLld,
+                Some("ld64.lld") => return LinkerFlavor::Darwin,
+                Some("ld.lld") => return LinkerFlavor::Gnu,
+                Some("ld.gold") => return LinkerFlavor::Gnu,
+                Some("mold") => return LinkerFlavor::Gnu,
+                Some("sold") => return LinkerFlavor::Gnu,
+                Some("wild") => return LinkerFlavor::Gnu,
+                _ => {}
+            }
+        }
+
+        match self.triple.environment {
+            target_lexicon::Environment::Gnu
+            | target_lexicon::Environment::Gnuabi64
+            | target_lexicon::Environment::Gnueabi
+            | target_lexicon::Environment::Gnueabihf
+            | target_lexicon::Environment::GnuLlvm => LinkerFlavor::Gnu,
+            target_lexicon::Environment::Musl => LinkerFlavor::Gnu,
+            target_lexicon::Environment::Android => LinkerFlavor::Gnu,
+            target_lexicon::Environment::Msvc => LinkerFlavor::Msvc,
+            target_lexicon::Environment::Macabi => LinkerFlavor::Darwin,
+            _ => match self.triple.operating_system {
+                OperatingSystem::Darwin(_) => LinkerFlavor::Darwin,
+                OperatingSystem::IOS(_) => LinkerFlavor::Darwin,
+                OperatingSystem::MacOSX(_) => LinkerFlavor::Darwin,
+                OperatingSystem::Linux => LinkerFlavor::Gnu,
+                OperatingSystem::Windows => LinkerFlavor::Msvc,
+                _ => match self.triple.architecture {
+                    target_lexicon::Architecture::Wasm32 => LinkerFlavor::WasmLld,
+                    target_lexicon::Architecture::Wasm64 => LinkerFlavor::WasmLld,
+                    _ => LinkerFlavor::Unsupported,
+                },
+            },
+        }
+    }
+
     /// Select the linker to use for this platform.
     ///
     /// We prefer to use the rust-lld linker when we can since it's usually there.
@@ -1750,25 +1943,25 @@ impl BuildRequest {
             return Ok(PathBuf::from(linker));
         }
 
-        let cc = match self.triple.operating_system {
-            OperatingSystem::Unknown if self.platform == Platform::Web => self.workspace.wasm_ld(),
+        if let Some(linker) = self.custom_linker.clone() {
+            return Ok(linker);
+        }
 
-            // The android clang linker is *special* and has some android-specific flags that we need
-            //
-            // Note that this is *clang*, not `lld`.
-            OperatingSystem::Linux if self.platform == Platform::Android => {
-                self.workspace.android_tools()?.android_cc(&self.triple)
-            }
+        let cc = match self.linker_flavor() {
+            LinkerFlavor::WasmLld => self.workspace.wasm_ld(),
 
             // On macOS, we use the system linker since it's usually there.
             // We could also use `lld` here, but it might not be installed by default.
             //
             // Note that this is *clang*, not `lld`.
-            OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => self.workspace.cc(),
+            LinkerFlavor::Darwin => self.workspace.cc(),
+
+            // On Linux, we use the system linker since it's usually there.
+            LinkerFlavor::Gnu => self.workspace.cc(),
 
             // On windows, instead of trying to find the system linker, we just go with the lld.link
             // that rustup provides. It's faster and more stable then reyling on link.exe in path.
-            OperatingSystem::Windows => self.workspace.lld_link(),
+            LinkerFlavor::Msvc => self.workspace.lld_link(),
 
             // The rest of the platforms use `cc` as the linker which should be available in your path,
             // provided you have build-tools setup. On mac/linux this is the default, but on Windows
@@ -1782,7 +1975,7 @@ impl BuildRequest {
             // Note that "cc" is *not* a linker. It's a compiler! The arguments we pass need to be in
             // the form of `-Wl,<args>` for them to make it to the linker. This matches how rust does it
             // which is confusing.
-            _ => self.workspace.cc(),
+            LinkerFlavor::Unsupported => self.workspace.cc(),
         };
 
         Ok(cc)
@@ -1827,7 +2020,7 @@ impl BuildRequest {
 
                 cmd.envs(rustc_args.envs.iter().cloned());
 
-                tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
+                // tracing::trace!("Setting env vars: {:#?}", rustc_args.envs);
 
                 Ok(cmd)
             }
@@ -1935,6 +2128,21 @@ impl BuildRequest {
             ));
         }
 
+        // Handle frameworks/dylibs by setting the rpath
+        // This is dependent on the bundle structure - in this case, appimage and appbundle for mac/linux
+        // todo: we need to figure out what to do for windows
+        match self.triple.operating_system {
+            OperatingSystem::Darwin(_) | OperatingSystem::IOS(_) => {
+                cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path/../Frameworks".to_string());
+                cargo_args.push("-Clink-arg=-Wl,-rpath,@executable_path".to_string());
+            }
+            OperatingSystem::Linux => {
+                cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN/../lib".to_string());
+                cargo_args.push("-Clink-arg=-Wl,-rpath,$ORIGIN".to_string());
+            }
+            _ => {}
+        }
+
         // Our fancy hot-patching engine needs a lot of customization to work properly.
         //
         // These args are mostly intended to be passed when *fat* linking but are generally fine to
@@ -2051,10 +2259,6 @@ impl BuildRequest {
             }));
         }
 
-        if let Some(target_dir) = self.custom_target_dir.as_ref() {
-            env_vars.push(("CARGO_TARGET_DIR", target_dir.display().to_string()));
-        }
-
         // If this is a release build, bake the base path and title into the binary with env vars.
         // todo: should we even be doing this? might be better being a build.rs or something else.
         if self.release {
@@ -2510,7 +2714,7 @@ impl BuildRequest {
     /// is "distributed" after building an application (configurable in the
     /// `Dioxus.toml`).
     fn internal_out_dir(&self) -> PathBuf {
-        let dir = self.workspace_dir().join("target").join("dx");
+        let dir = self.target_dir.join("dx");
         std::fs::create_dir_all(&dir).unwrap();
         dir
     }

+ 1 - 1
packages/cli/src/cli/link.rs

@@ -44,8 +44,8 @@ pub enum LinkerFlavor {
     Gnu,
     Darwin,
     WasmLld,
-    Unix,
     Msvc,
+    Unsupported, // a catch-all for unsupported linkers, usually the stripped-down unix ones
 }
 
 impl LinkAction {

+ 0 - 10
packages/cli/src/devcfg.rs

@@ -1,15 +1,5 @@
 //! Configuration of the CLI at runtime to enable certain experimental features.
 
-use std::path::Path;
-
-/// Should we cache the dependency library?
-///
-/// When the `DIOXUS_CACHE_DEP_LIB` environment variable is set, we will cache the dependency library
-/// built from the target's dependencies.
-pub(crate) fn should_cache_dep_lib(lib: &Path) -> bool {
-    std::env::var("DIOXUS_CACHE_DEP_LIB").is_ok() && lib.exists()
-}
-
 /// Should we force the entropy to be used on the main exe?
 ///
 /// This is used to verify that binaries are copied with different names such that they don't collide

+ 2 - 0
packages/cli/src/rustcwrapper.rs

@@ -25,6 +25,7 @@ pub fn is_wrapping_rustc() -> bool {
 pub struct RustcArgs {
     pub args: Vec<String>,
     pub envs: Vec<(String, String)>,
+    pub link_args: Vec<String>,
 }
 
 /// Run rustc directly, but output the result to a file.
@@ -50,6 +51,7 @@ pub async fn run_rustc() {
     let rustc_args = RustcArgs {
         args: args().skip(1).collect::<Vec<_>>(),
         envs: vars().collect::<_>(),
+        link_args: Default::default(),
     };
 
     std::fs::create_dir_all(var_file.parent().expect("Failed to get parent dir"))

+ 7 - 1
packages/cli/src/serve/output.rs

@@ -721,7 +721,13 @@ impl Output {
         frame.render_widget(
             Paragraph::new(Line::from(vec![
                 "Hotreload: ".gray(),
-                "rsx and assets".yellow(),
+                if !state.runner.automatic_rebuilds {
+                    "disabled".dark_gray()
+                } else if state.runner.use_hotpatch_engine {
+                    "hot-patching".yellow()
+                } else {
+                    "rsx and assets".yellow()
+                },
             ])),
             meta_list[3],
         );

+ 4 - 3
packages/cli/src/serve/proxy.rs

@@ -138,9 +138,10 @@ pub(crate) fn proxy_to(
         match res {
             Ok(res) => {
                 // log assets at a different log level
-                if uri.path().starts_with("/assets")
-                    || uri.path().starts_with("/_dioxus")
-                    || uri.path().starts_with("/public")
+                if uri.path().starts_with("/assets/")
+                    || uri.path().starts_with("/_dioxus/")
+                    || uri.path().starts_with("/public/")
+                    || uri.path().starts_with("/wasm/")
                 {
                     tracing::trace!(dx_src = ?TraceSrc::Dev, "[{}] {}", res.status().as_u16(), uri);
                 } else {

+ 3 - 3
packages/cli/src/serve/runner.rs

@@ -341,15 +341,15 @@ impl AppServer {
                     continue;
                 };
 
+                // Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version
+                cached_file.most_recent = Some(new_contents);
+
                 // This assumes the two files are structured similarly. If they're not, we can't diff them
                 let Some(changed_rsx) = dioxus_rsx_hotreload::diff_rsx(&new_file, &old_file) else {
                     needs_full_rebuild = true;
                     break;
                 };
 
-                // Update the most recent version of the file, so when we force a rebuild, we keep operating on the most recent version
-                cached_file.most_recent = Some(new_contents);
-
                 for ChangedRsx { old, new } in changed_rsx {
                     let old_start = old.span().start();
 

+ 258 - 104
packages/desktop/src/launch.rs

@@ -1,3 +1,228 @@
+// use crate::Config;
+// use crate::{
+//     app::App,
+//     ipc::{IpcMethod, UserWindowEvent},
+// };
+// use dioxus_core::*;
+// use dioxus_document::eval;
+// use std::any::Any;
+// use tao::event::{Event, StartCause, WindowEvent};
+
+// /// Launches the WebView and runs the event loop
+// pub fn launch(root: fn() -> Element) {
+//     launch_cfg(root, vec![], vec![]);
+// }
+
+// /// Launches the WebView and runs the event loop, with configuration and root props.
+// pub fn launch_cfg(
+//     root: fn() -> Element,
+//     contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+//     platform_config: Vec<Box<dyn Any>>,
+// ) {
+//     let mut virtual_dom = VirtualDom::new(root);
+
+//     for context in contexts {
+//         virtual_dom.insert_any_root_context(context());
+//     }
+
+//     let platform_config = *platform_config
+//         .into_iter()
+//         .find_map(|cfg| cfg.downcast::<Config>().ok())
+//         .unwrap_or_default();
+
+//     launch_virtual_dom(virtual_dom, platform_config)
+// }
+
+// /// Launches the WebView and runs the event loop, with configuration and root props.
+// pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
+//     #[cfg(feature = "tokio_runtime")]
+//     {
+//         tokio::runtime::Builder::new_multi_thread()
+//             .enable_all()
+//             .build()
+//             .unwrap()
+//             .block_on(tokio::task::unconstrained(async move {
+//                 launch_virtual_dom_blocking(virtual_dom, desktop_config)
+//             }));
+
+//         unreachable!("The desktop launch function will never exit")
+//     }
+
+//     #[cfg(not(feature = "tokio_runtime"))]
+//     {
+//         launch_virtual_dom_blocking(virtual_dom, desktop_config);
+//     }
+// }
+
+// /// Launch the WebView and run the event loop, with configuration and root props.
+// ///
+// /// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
+// /// and is equivalent to calling launch_with_props with the tokio feature disabled.
+// pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config: Config) -> ! {
+//     let mut custom_event_handler = desktop_config.custom_event_handler.take();
+//     let (event_loop, mut app) = App::new(desktop_config, virtual_dom);
+
+//     event_loop.run(move |window_event, event_loop, control_flow| {
+//         // Set the control flow and check if any events need to be handled in the app itself
+//         app.tick(&window_event);
+
+//         if let Some(ref mut f) = custom_event_handler {
+//             f(&window_event, event_loop)
+//         }
+
+//         match window_event {
+//             Event::NewEvents(StartCause::Init) => app.handle_start_cause_init(),
+//             Event::LoopDestroyed => app.handle_loop_destroyed(),
+//             Event::WindowEvent {
+//                 event, window_id, ..
+//             } => match event {
+//                 WindowEvent::CloseRequested => app.handle_close_requested(window_id),
+//                 WindowEvent::Destroyed { .. } => app.window_destroyed(window_id),
+//                 WindowEvent::Resized(new_size) => app.resize_window(window_id, new_size),
+//                 _ => {}
+//             },
+
+//             Event::UserEvent(event) => match event {
+//                 UserWindowEvent::Poll(id) => app.poll_vdom(id),
+//                 UserWindowEvent::NewWindow => app.handle_new_window(),
+//                 UserWindowEvent::CloseWindow(id) => app.handle_close_msg(id),
+//                 UserWindowEvent::Shutdown => app.control_flow = tao::event_loop::ControlFlow::Exit,
+
+//                 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+//                 UserWindowEvent::GlobalHotKeyEvent(evnt) => app.handle_global_hotkey(evnt),
+
+//                 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+//                 UserWindowEvent::MudaMenuEvent(evnt) => app.handle_menu_event(evnt),
+
+//                 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+//                 UserWindowEvent::TrayMenuEvent(evnt) => app.handle_tray_menu_event(evnt),
+
+//                 #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
+//                 UserWindowEvent::TrayIconEvent(evnt) => app.handle_tray_icon_event(evnt),
+
+//                 #[cfg(all(feature = "devtools", debug_assertions))]
+//                 UserWindowEvent::HotReloadEvent(msg) => app.handle_hot_reload_msg(msg),
+
+//                 // Windows-only drag-n-drop fix events. We need to call the interpreter drag-n-drop code.
+//                 UserWindowEvent::WindowsDragDrop(id) => {
+//                     if let Some(webview) = app.webviews.get(&id) {
+//                         webview.dom.in_runtime(|| {
+//                             ScopeId::ROOT.in_runtime(|| {
+//                                 eval("window.interpreter.handleWindowsDragDrop();");
+//                             });
+//                         });
+//                     }
+//                 }
+//                 UserWindowEvent::WindowsDragLeave(id) => {
+//                     if let Some(webview) = app.webviews.get(&id) {
+//                         webview.dom.in_runtime(|| {
+//                             ScopeId::ROOT.in_runtime(|| {
+//                                 eval("window.interpreter.handleWindowsDragLeave();");
+//                             });
+//                         });
+//                     }
+//                 }
+//                 UserWindowEvent::WindowsDragOver(id, x_pos, y_pos) => {
+//                     if let Some(webview) = app.webviews.get(&id) {
+//                         webview.dom.in_runtime(|| {
+//                             ScopeId::ROOT.in_runtime(|| {
+//                                 let e = eval(
+//                                     r#"
+//                                     const xPos = await dioxus.recv();
+//                                     const yPos = await dioxus.recv();
+//                                     window.interpreter.handleWindowsDragOver(xPos, yPos)
+//                                     "#,
+//                                 );
+
+//                                 _ = e.send(x_pos);
+//                                 _ = e.send(y_pos);
+//                             });
+//                         });
+//                     }
+//                 }
+
+//                 UserWindowEvent::Ipc { id, msg } => match msg.method() {
+//                     IpcMethod::Initialize => app.handle_initialize_msg(id),
+//                     IpcMethod::FileDialog => app.handle_file_dialog_msg(msg, id),
+//                     IpcMethod::UserEvent => {}
+//                     IpcMethod::Query => app.handle_query_msg(msg, id),
+//                     IpcMethod::BrowserOpen => app.handle_browser_open(msg),
+//                     IpcMethod::Other(_) => {}
+//                 },
+//             },
+//             _ => {}
+//         }
+
+//         *control_flow = app.control_flow;
+//     })
+// }
+
+// /// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
+// /// We hardcode these to have a single trampoline for host Java code to call into.
+// ///
+// /// This saves us from having to plumb the top-level package name all the way down into
+// /// this file. This is better for modularity (ie just call dioxus' main to run the app) as
+// /// well as cache thrashing since this crate doesn't rely on external env vars.
+// ///
+// /// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
+// /// change this, you'll need to change the CLI as well.
+// #[cfg(target_os = "android")]
+// #[no_mangle]
+// #[inline(never)]
+// pub extern "C" fn start_app() {
+//     wry::android_binding!(dev_dioxus, main, wry);
+//     tao::android_binding!(
+//         dev_dioxus,
+//         main,
+//         WryActivity,
+//         wry::android_setup,
+//         dioxus_main_root_fn,
+//         tao
+//     );
+// }
+
+// #[cfg(target_os = "android")]
+// #[doc(hidden)]
+// pub fn dioxus_main_root_fn() {
+//     // we're going to find the `main` symbol using dlsym directly and call it
+//     unsafe {
+//         let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
+
+//         if main_fn_ptr.is_null() {
+//             main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
+//         }
+
+//         if main_fn_ptr.is_null() {
+//             panic!("Failed to find main symbol");
+//         }
+
+//         // Set the env vars that rust code might expect, passed off to us by the android app
+//         // Doing this before main emulates the behavior of a regular executable
+//         if cfg!(target_os = "android") && cfg!(debug_assertions) {
+//             load_env_file_from_session_cache();
+//         }
+
+//         let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
+//         main_fn();
+//     };
+// }
+
+// /// Load the env file from the session cache if we're in debug mode and on android
+// ///
+// /// This is a slightly hacky way of being able to use std::env::var code in android apps without
+// /// going through their custom java-based system.
+// #[cfg(target_os = "android")]
+// fn load_env_file_from_session_cache() {
+//     let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
+//     if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
+//         for line in env_file.lines() {
+//             if let Some((key, value)) = line.trim().split_once('=') {
+//                 std::env::set_var(key, value);
+//             }
+//         }
+//     }
+// }
+
 use crate::Config;
 use crate::{
     app::App,
@@ -8,52 +233,6 @@ use dioxus_document::eval;
 use std::any::Any;
 use tao::event::{Event, StartCause, WindowEvent};
 
-/// Launches the WebView and runs the event loop
-pub fn launch(root: fn() -> Element) {
-    launch_cfg(root, vec![], vec![]);
-}
-
-/// Launches the WebView and runs the event loop, with configuration and root props.
-pub fn launch_cfg(
-    root: fn() -> Element,
-    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
-    platform_config: Vec<Box<dyn Any>>,
-) {
-    let mut virtual_dom = VirtualDom::new(root);
-
-    for context in contexts {
-        virtual_dom.insert_any_root_context(context());
-    }
-
-    let platform_config = *platform_config
-        .into_iter()
-        .find_map(|cfg| cfg.downcast::<Config>().ok())
-        .unwrap_or_default();
-
-    launch_virtual_dom(virtual_dom, platform_config)
-}
-
-/// Launches the WebView and runs the event loop, with configuration and root props.
-pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
-    #[cfg(feature = "tokio_runtime")]
-    {
-        tokio::runtime::Builder::new_multi_thread()
-            .enable_all()
-            .build()
-            .unwrap()
-            .block_on(tokio::task::unconstrained(async move {
-                launch_virtual_dom_blocking(virtual_dom, desktop_config)
-            }));
-
-        unreachable!("The desktop launch function will never exit")
-    }
-
-    #[cfg(not(feature = "tokio_runtime"))]
-    {
-        launch_virtual_dom_blocking(virtual_dom, desktop_config);
-    }
-}
-
 /// Launch the WebView and run the event loop, with configuration and root props.
 ///
 /// This will block the main thread, and *must* be spawned on the main thread. This function does not assume any runtime
@@ -157,68 +336,43 @@ pub fn launch_virtual_dom_blocking(virtual_dom: VirtualDom, mut desktop_config:
     })
 }
 
-/// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
-/// We hardcode these to have a single trampoline for host Java code to call into.
-///
-/// This saves us from having to plumb the top-level package name all the way down into
-/// this file. This is better for modularity (ie just call dioxus' main to run the app) as
-/// well as cache thrashing since this crate doesn't rely on external env vars.
-///
-/// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
-/// change this, you'll need to change the CLI as well.
-#[cfg(target_os = "android")]
-#[no_mangle]
-#[inline(never)]
-pub extern "C" fn start_app() {
-    wry::android_binding!(dev_dioxus, main, wry);
-    tao::android_binding!(
-        dev_dioxus,
-        main,
-        WryActivity,
-        wry::android_setup,
-        dioxus_main_root_fn,
-        tao
-    );
-}
+/// Launches the WebView and runs the event loop, with configuration and root props.
+pub fn launch_virtual_dom(virtual_dom: VirtualDom, desktop_config: Config) -> ! {
+    #[cfg(feature = "tokio_runtime")]
+    {
+        tokio::runtime::Builder::new_multi_thread()
+            .enable_all()
+            .build()
+            .unwrap()
+            .block_on(tokio::task::unconstrained(async move {
+                launch_virtual_dom_blocking(virtual_dom, desktop_config)
+            }));
 
-#[cfg(target_os = "android")]
-#[doc(hidden)]
-pub fn dioxus_main_root_fn() {
-    // we're going to find the `main` symbol using dlsym directly and call it
-    unsafe {
-        let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
+        unreachable!("The desktop launch function will never exit")
+    }
 
-        if main_fn_ptr.is_null() {
-            main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
-        }
+    #[cfg(not(feature = "tokio_runtime"))]
+    {
+        launch_virtual_dom_blocking(virtual_dom, desktop_config);
+    }
+}
 
-        if main_fn_ptr.is_null() {
-            panic!("Failed to find main symbol");
-        }
+/// Launches the WebView and runs the event loop, with configuration and root props.
+pub fn launch(
+    root: fn() -> Element,
+    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+    platform_config: Vec<Box<dyn Any>>,
+) -> ! {
+    let mut virtual_dom = VirtualDom::new(root);
 
-        // Set the env vars that rust code might expect, passed off to us by the android app
-        // Doing this before main emulates the behavior of a regular executable
-        if cfg!(target_os = "android") && cfg!(debug_assertions) {
-            load_env_file_from_session_cache();
-        }
+    for context in contexts {
+        virtual_dom.insert_any_root_context(context());
+    }
 
-        let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
-        main_fn();
-    };
-}
+    let platform_config = *platform_config
+        .into_iter()
+        .find_map(|cfg| cfg.downcast::<Config>().ok())
+        .unwrap_or_default();
 
-/// Load the env file from the session cache if we're in debug mode and on android
-///
-/// This is a slightly hacky way of being able to use std::env::var code in android apps without
-/// going through their custom java-based system.
-#[cfg(target_os = "android")]
-fn load_env_file_from_session_cache() {
-    let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
-    if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
-        for line in env_file.lines() {
-            if let Some((key, value)) = line.trim().split_once('=') {
-                std::env::set_var(key, value);
-            }
-        }
-    }
+    launch_virtual_dom(virtual_dom, platform_config)
 }

+ 1 - 1
packages/devtools/src/lib.rs

@@ -80,7 +80,7 @@ pub fn connect_at(endpoint: String, mut callback: impl FnMut(DevserverMsg) + Sen
     std::thread::spawn(move || {
         let uri = format!(
             "{endpoint}?aslr_reference={}&build_id={}",
-            subsecond::__aslr_reference(),
+            subsecond::aslr_reference(),
             dioxus_cli_config::build_id()
         );
 

+ 2 - 2
packages/dioxus/src/launch.rs

@@ -332,12 +332,12 @@ impl LaunchBuilder {
 
         #[cfg(feature = "mobile")]
         if matches!(platform, KnownPlatform::Mobile) {
-            return dioxus_desktop::launch::launch_cfg(app, contexts, configs);
+            return dioxus_mobile::launch_bindings::launch(app, contexts, configs);
         }
 
         #[cfg(feature = "desktop")]
         if matches!(platform, KnownPlatform::Desktop) {
-            return dioxus_desktop::launch::launch_cfg(app, contexts, configs);
+            return dioxus_desktop::launch::launch(app, contexts, configs);
         }
 
         #[cfg(feature = "server")]

+ 160 - 1
packages/mobile/src/lib.rs

@@ -5,6 +5,23 @@
 pub use dioxus_desktop::*;
 use dioxus_lib::prelude::*;
 use std::any::Any;
+use std::sync::Mutex;
+
+pub mod launch_bindings {
+
+    use super::*;
+    pub fn launch(
+        root: fn() -> Element,
+        contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+        platform_config: Vec<Box<dyn Any>>,
+    ) {
+        super::launch_cfg(root, contexts, platform_config);
+    }
+
+    pub fn launch_virtual_dom(_virtual_dom: VirtualDom, _desktop_config: Config) -> ! {
+        todo!()
+    }
+}
 
 /// Launch via the binding API
 pub fn launch(root: fn() -> Element) {
@@ -16,5 +33,147 @@ pub fn launch_cfg(
     contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
     platform_config: Vec<Box<dyn Any>>,
 ) {
-    dioxus_desktop::launch::launch_cfg(root, contexts, platform_config);
+    #[cfg(target_os = "android")]
+    {
+        *APP_OBJECTS.lock().unwrap() = Some(BoundLaunchObjects {
+            root,
+            contexts,
+            platform_config,
+        });
+    }
+
+    #[cfg(not(target_os = "android"))]
+    {
+        dioxus_desktop::launch::launch(root, contexts, platform_config);
+    }
+}
+
+/// We need to store the root function and contexts in a static so that when the tao bindings call
+/// "start_app", that the original function arguments are still around.
+///
+/// If you look closely, you'll notice that we impl Send for this struct. This would normally be
+/// unsound. However, we know that the thread that created these objects ("main()" - see JNI_OnLoad)
+/// is finished once `start_app` is called. This is similar to how an Rc<T> is technically safe
+/// to move between threads if you can prove that no other thread is using the Rc<T> at the same time.
+/// Crates like https://crates.io/crates/sendable exist that build on this idea but with runtimk,
+/// validation that the current thread is the one that created the object.
+///
+/// Since `main()` completes, the only reader of this data will be `start_app`, so it's okay to
+/// impl this as Send/Sync.
+///
+/// Todo(jon): the visibility of functions in this module is too public. Make sure to hide them before
+/// releasing 0.7.
+struct BoundLaunchObjects {
+    root: fn() -> Element,
+    contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+    platform_config: Vec<Box<dyn Any>>,
+}
+
+unsafe impl Send for BoundLaunchObjects {}
+unsafe impl Sync for BoundLaunchObjects {}
+
+static APP_OBJECTS: Mutex<Option<BoundLaunchObjects>> = Mutex::new(None);
+
+#[doc(hidden)]
+pub fn root() {
+    let app = APP_OBJECTS
+        .lock()
+        .expect("APP_FN_PTR lock failed")
+        .take()
+        .expect("Android to have set the app trampoline");
+
+    let BoundLaunchObjects {
+        root,
+        contexts,
+        platform_config,
+    } = app;
+
+    dioxus_desktop::launch::launch(root, contexts, platform_config);
 }
+
+/// Expose the `Java_dev_dioxus_main_WryActivity_create` function to the JNI layer.
+/// We hardcode these to have a single trampoline for host Java code to call into.
+///
+/// This saves us from having to plumb the top-level package name all the way down into
+/// this file. This is better for modularity (ie just call dioxus' main to run the app) as
+/// well as cache thrashing since this crate doesn't rely on external env vars.
+///
+/// The CLI is expecting to find `dev.dioxus.main` in the final library. If you find a need to
+/// change this, you'll need to change the CLI as well.
+#[cfg(target_os = "android")]
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn start_app() {
+    tao::android_binding!(dev_dioxus, main, WryActivity, wry::android_setup, root, tao);
+    wry::android_binding!(dev_dioxus, main, wry);
+}
+
+/// Call our `main` function to initialize the rust runtime and set the launch binding trampoline
+#[cfg(target_os = "android")]
+#[no_mangle]
+#[inline(never)]
+pub extern "C" fn JNI_OnLoad(
+    _vm: *mut libc::c_void,
+    _reserved: *mut libc::c_void,
+) -> jni::sys::jint {
+    // we're going to find the `main` symbol using dlsym directly and call it
+    unsafe {
+        let mut main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"main\0".as_ptr() as _);
+
+        if main_fn_ptr.is_null() {
+            main_fn_ptr = libc::dlsym(libc::RTLD_DEFAULT, b"_main\0".as_ptr() as _);
+        }
+
+        if main_fn_ptr.is_null() {
+            panic!("Failed to find main symbol");
+        }
+
+        // Set the env vars that rust code might expect, passed off to us by the android app
+        // Doing this before main emulates the behavior of a regular executable
+        if cfg!(target_os = "android") && cfg!(debug_assertions) {
+            load_env_file_from_session_cache();
+        }
+
+        let main_fn: extern "C" fn() = std::mem::transmute(main_fn_ptr);
+        main_fn();
+    };
+
+    jni::sys::JNI_VERSION_1_6
+}
+
+/// Load the env file from the session cache if we're in debug mode and on android
+///
+/// This is a slightly hacky way of being able to use std::env::var code in android apps without
+/// going through their custom java-based system.
+#[cfg(target_os = "android")]
+fn load_env_file_from_session_cache() {
+    let env_file = dioxus_cli_config::android_session_cache_dir().join(".env");
+    if let Some(env_file) = std::fs::read_to_string(&env_file).ok() {
+        for line in env_file.lines() {
+            if let Some((key, value)) = line.trim().split_once('=') {
+                std::env::set_var(key, value);
+            }
+        }
+    }
+}
+
+// #![doc = include_str!("../README.md")]
+// #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
+// #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
+
+// pub use dioxus_desktop::*;
+// use dioxus_lib::prelude::*;
+// use std::any::Any;
+
+// /// Launch via the binding API
+// pub fn launch(root: fn() -> Element) {
+//     launch_cfg(root, vec![], vec![]);
+// }
+
+// pub fn launch_cfg(
+//     root: fn() -> Element,
+//     contexts: Vec<Box<dyn Fn() -> Box<dyn Any> + Send + Sync>>,
+//     platform_config: Vec<Box<dyn Any>>,
+// ) {
+//     dioxus_desktop::launch::launch_cfg(root, contexts, platform_config);
+// }

+ 12 - 13
packages/server/src/config.rs

@@ -354,19 +354,18 @@ impl ServeConfigBuilder {
                 let index_path = self
                     .index_path
                     .unwrap_or_else(|| public_path.join("index.html"));
-                load_index_path(index_path)?
-                // load_index_path(index_path).unwrap_or_else(|_| {
-                //     r#"
-                //     <!DOCTYPE html>
-                //     <html>
-                //         <head> </head>
-                //         <body>
-                //             <div id="main"></div>
-                //         </body>
-                //     </html>
-                //     "#
-                //     .to_string()
-                // })
+                load_index_path(index_path).unwrap_or_else(|_| {
+                    r#"
+                    <!DOCTYPE html>
+                    <html>
+                        <head> </head>
+                        <body>
+                            <div id="main"></div>
+                        </body>
+                    </html>
+                    "#
+                    .to_string()
+                })
             }
         };
 

+ 11 - 0
packages/subsecond/subsecond-tests/cross-tls-crate-dylib/Cargo.toml

@@ -0,0 +1,11 @@
+[package]
+name = "cross-tls-crate-dylib"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+# when testing, use a dylib
+# crate-type = ["dylib"]
+
+[dependencies]

+ 25 - 0
packages/subsecond/subsecond-tests/cross-tls-crate-dylib/src/lib.rs

@@ -0,0 +1,25 @@
+pub use std::cell::Cell;
+use std::{cell::RefCell, thread::LocalKey};
+
+#[derive(Debug)]
+pub struct StoredItem {
+    pub name: String,
+    pub value: f32,
+    pub items: Vec<String>,
+}
+
+thread_local! {
+    pub static BAZ: RefCell<Option<StoredItem>> = const { RefCell::new(None) };
+}
+
+pub fn get_baz() -> &'static LocalKey<RefCell<Option<StoredItem>>> {
+    if BAZ.with(|f| f.borrow().is_none()) {
+        BAZ.set(Some(StoredItem {
+            name: "BAR".to_string(),
+            value: 0.0,
+            items: vec!["item1".to_string(), "item2".to_string()],
+        }));
+    }
+
+    &BAZ
+}

+ 7 - 0
packages/subsecond/subsecond-tests/cross-tls-crate/Cargo.toml

@@ -0,0 +1,7 @@
+[package]
+name = "cross-tls-crate"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[dependencies]

+ 25 - 0
packages/subsecond/subsecond-tests/cross-tls-crate/src/lib.rs

@@ -0,0 +1,25 @@
+pub use std::cell::Cell;
+use std::{cell::RefCell, thread::LocalKey};
+
+#[derive(Debug)]
+pub struct StoredItem {
+    pub name: String,
+    pub value: f32,
+    pub items: Vec<String>,
+}
+
+thread_local! {
+    pub static BAR: RefCell<Option<StoredItem>> = const { RefCell::new(None) };
+}
+
+pub fn get_bar() -> &'static LocalKey<RefCell<Option<StoredItem>>> {
+    if BAR.with(|f| f.borrow().is_none()) {
+        BAR.set(Some(StoredItem {
+            name: "BAR".to_string(),
+            value: 0.0,
+            items: vec!["item1".to_string(), "item2".to_string()],
+        }));
+    }
+
+    &BAR
+}

+ 10 - 0
packages/subsecond/subsecond-tests/cross-tls-test/Cargo.toml

@@ -0,0 +1,10 @@
+[package]
+name = "subsecond-tls-harness"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[dependencies]
+dioxus-devtools = { workspace = true }
+cross-tls-crate = { path = "../cross-tls-crate" }
+cross-tls-crate-dylib = { path = "../cross-tls-crate-dylib" }

+ 13 - 0
packages/subsecond/subsecond-tests/cross-tls-test/README.md

@@ -0,0 +1,13 @@
+harness for testing cross-crate TLS imports.
+
+TLS is the hardest to get right with binary patchinig since we need to rewrite initializers.
+
+this crate relies on two crates - one as a dylib and the other as an rlib
+
+the dylib should make it into our bundle and the rlib should just be linked.
+
+both shouldn't cause access errors or segfaults during hotpatch.
+
+```sh
+cargo run --package dioxus-cli -- serve --hotpatch
+```

+ 33 - 0
packages/subsecond/subsecond-tests/cross-tls-test/src/main.rs

@@ -0,0 +1,33 @@
+use std::{cell::Cell, thread, time::Duration};
+
+use cross_tls_crate::get_bar;
+use cross_tls_crate_dylib::get_baz;
+
+fn main() {
+    dioxus_devtools::connect_subsecond();
+    loop {
+        dioxus_devtools::subsecond::call(|| {
+            use cross_tls_crate::BAR;
+            use cross_tls_crate_dylib::BAZ;
+
+            thread_local! {
+                pub static FOO: Cell<f32> = const { Cell::new(2.0) };
+            }
+
+            println!("Hello  123s123123s: {}", FOO.get());
+            get_bar().with(|f| println!("Bar: {:?}", f.borrow()));
+            thread::sleep(Duration::from_secs(1));
+
+            FOO.set(2.0);
+            get_bar().with(|f| f.borrow_mut().as_mut().unwrap().value = 3.0);
+            get_baz().with(|f| f.borrow_mut().as_mut().unwrap().value = 4.0);
+
+            BAR.with_borrow(|f| {
+                println!("Bar: {:?}", f);
+            });
+            BAZ.with_borrow(|f| {
+                println!("Baz: {:?}", f);
+            });
+        });
+    }
+}

+ 42 - 16
packages/subsecond/subsecond/src/lib.rs

@@ -495,10 +495,9 @@ pub unsafe fn apply_patch(mut table: JumpTable) -> Result<(), PatchError> {
             }
         }));
 
-        // Use the `__aslr_reference` symbol as a sentinel for the current executable. This is basically a
-        // cross-platform version of `__mh_execute_header` on macOS that sets a reference point for the
-        // jump table.
-        let old_offset = __aslr_reference() - table.aslr_reference as usize;
+        // Use the `main` symbol as a sentinel for the current executable. This is basically a
+        // cross-platform version of `__mh_execute_header` on macOS that we can use to base the executable.
+        let old_offset = aslr_reference() - table.aslr_reference as usize;
 
         // Use the `main` symbol as a sentinel for the loaded library. Might want to move away
         // from this at some point, or make it configurable
@@ -672,22 +671,49 @@ pub enum PatchError {
     AndroidMemfd(String),
 }
 
-/// This function returns its own address, providing a stable reference point for hot-patch engine
-/// to hook onto. If you were to write an object file for this function, it would amount to:
-///
-/// ```asm
-/// __aslr_reference:
-///         mov     rax, qword ptr [rip + __aslr_reference@GOTPCREL] // notice the @GOTPCREL relocation
-///         ret
-/// ```
+/// This function returns the address of the main function in the current executable. This is used as
+/// an anchor to reference the current executable's base address.
 ///
 /// The point here being that we have a stable address both at runtime and compile time, making it
 /// possible to calculate the ASLR offset from within the process to correct the jump table.
+///
+/// It should only be called from the main executable *first* and not from a shared library since it
+/// self-initializes.
 #[doc(hidden)]
-#[inline(never)]
-#[no_mangle]
-pub extern "C" fn __aslr_reference() -> usize {
-    __aslr_reference as *const () as usize
+pub fn aslr_reference() -> usize {
+    #[cfg(target_family = "wasm")]
+    return 0;
+
+    #[cfg(not(target_family = "wasm"))]
+    unsafe {
+        use std::ffi::c_void;
+
+        // The first call to this function should occur in the
+        static mut MAIN_PTR: *mut c_void = std::ptr::null_mut();
+
+        if MAIN_PTR.is_null() {
+            #[cfg(unix)]
+            {
+                MAIN_PTR = libc::dlsym(libc::RTLD_DEFAULT, c"main".as_ptr() as _);
+            }
+
+            #[cfg(windows)]
+            {
+                extern "system" {
+                    fn GetModuleHandleA(lpModuleName: *const i8) -> *mut std::ffi::c_void;
+                    fn GetProcAddress(
+                        hModule: *mut std::ffi::c_void,
+                        lpProcName: *const i8,
+                    ) -> *mut std::ffi::c_void;
+                }
+
+                MAIN_PTR =
+                    GetProcAddress(GetModuleHandleA(std::ptr::null()), c"main".as_ptr() as _) as _;
+            }
+        }
+
+        MAIN_PTR as usize
+    }
 }
 
 /// On Android, we can't dlopen libraries that aren't placed inside /data/data/<package_name>/lib/