use std::fs;
use polyplug::logger::LoggerHandle;
use polyplug_abi::types::LogLevel;
use std::io::Write as _;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use netcorehost::hostfxr::AssemblyDelegateLoader;
use netcorehost::hostfxr::HostfxrContext;
use netcorehost::hostfxr::InitializedForRuntimeConfig;
use netcorehost::hostfxr::ManagedFunction;
use netcorehost::pdcstring::PdCString;
use once_cell::sync::OnceCell;
use polyplug::error::LoaderError;
use crate::config::DotnetConfig;
use crate::config::HostfxrLocation;
pub(crate) type InitFn = unsafe extern "system" fn(
*const polyplug_abi::HostApi,
*const polyplug_abi::BundleInitContext,
) -> polyplug_abi::types::AbiError;
pub(crate) const BYTE_BRIDGE_DLL: &[u8] = include_bytes!(env!("POLYPLUG_DOTNET_BYTE_BRIDGE_DLL"));
pub(crate) const BYTE_BRIDGE_AVAILABLE: bool = matches!(
env!("POLYPLUG_DOTNET_BYTE_BRIDGE_AVAILABLE").as_bytes(),
b"1"
);
pub(crate) type BridgeLoadAndGetInitFn =
unsafe extern "system" fn(u64, u64, *const u8, i32, *const u8, i32) -> *const core::ffi::c_void;
pub(crate) type BridgeLoadFromPathAndGetInitFn =
unsafe extern "system" fn(u64, u64, *const u8, i32, *const u8, i32) -> *const core::ffi::c_void;
pub(crate) type BridgePreloadDependencyFn =
unsafe extern "system" fn(u64, u64, *const u8, i32) -> u32;
pub(crate) type BridgeUnloadFn = unsafe extern "system" fn(u64, u64) -> u32;
pub(crate) type BridgeIsAlcAliveFn = unsafe extern "system" fn(u64, u64) -> u32;
pub(crate) struct DotnetContext {
_context: Mutex<HostfxrContext<InitializedForRuntimeConfig>>,
bridge: OnceCell<ByteBridge>,
}
struct ByteBridge {
load_and_get_init: BridgeLoadAndGetInitFn,
load_from_path_and_get_init: BridgeLoadFromPathAndGetInitFn,
preload_dependency: BridgePreloadDependencyFn,
unload: BridgeUnloadFn,
is_alc_alive: BridgeIsAlcAliveFn,
_staged_dir: tempfile::TempDir,
}
unsafe impl Send for ByteBridge {}
unsafe impl Sync for ByteBridge {}
pub(crate) static CLR_CONTEXT: OnceCell<Arc<DotnetContext>> = OnceCell::new();
pub(crate) fn init_context(
config: &DotnetConfig,
bundle_dir: &std::path::Path,
logger: LoggerHandle,
) -> Result<Arc<DotnetContext>, LoaderError> {
let ver_str: &str = config
.min_framework
.strip_prefix("net")
.unwrap_or(&config.min_framework);
let full_version: String = if ver_str.chars().filter(|c: &char| *c == '.').count() == 1 {
format!("{ver_str}.0")
} else {
ver_str.to_owned()
};
let json: String = serde_json::json!({
"runtimeOptions": {
"tfm": config.min_framework,
"framework": {
"name": "Microsoft.NETCore.App",
"version": full_version
},
"additionalProbingPaths": [bundle_dir.to_string_lossy()]
}
})
.to_string();
let mut tmp: tempfile::NamedTempFile = tempfile::Builder::new()
.suffix(".json")
.tempfile()
.map_err(|e: std::io::Error| LoaderError::InitFailed {
bundle: "<tempfile>".to_owned(),
error: format!("CLR init failed: {}", e),
})?;
tmp.write_all(json.as_bytes())
.map_err(|e: std::io::Error| LoaderError::InitFailed {
bundle: "<tempfile>".to_owned(),
error: format!("CLR init failed: {}", e),
})?;
tmp.flush()
.map_err(|e: std::io::Error| LoaderError::InitFailed {
bundle: "<tempfile>".to_owned(),
error: format!("CLR init failed: {}", e),
})?;
let tmp_path_guard: tempfile::TempPath = tmp.into_temp_path();
let temp_path: PathBuf = tmp_path_guard.to_path_buf();
let pdcpath: PdCString =
PdCString::from_os_str(temp_path.as_os_str()).map_err(|_| LoaderError::InitFailed {
bundle: temp_path.to_string_lossy().into_owned(),
error: "CLR init failed: runtimeconfig path contains embedded nul byte".to_owned(),
})?;
let hostfxr: netcorehost::hostfxr::Hostfxr = match &config.hostfxr {
HostfxrLocation::Auto => {
let fxr_path: PathBuf =
find_hostfxr_auto(logger).ok_or_else(|| LoaderError::InitFailed {
bundle: "<hostfxr>".to_owned(),
error: "CLR init failed: hostfxr not found; install .NET or set DOTNET_ROOT"
.to_owned(),
})?;
netcorehost::hostfxr::Hostfxr::load_from_path(&fxr_path).map_err(|e| {
LoaderError::InitFailed {
bundle: fxr_path.to_string_lossy().into_owned(),
error: format!("CLR init failed: {}", e),
}
})?
}
HostfxrLocation::Path(p) => {
netcorehost::hostfxr::Hostfxr::load_from_path(p).map_err(|e| {
LoaderError::InitFailed {
bundle: p.to_string_lossy().into_owned(),
error: format!("CLR init failed: {}", e),
}
})?
}
};
let context: HostfxrContext<InitializedForRuntimeConfig> = hostfxr
.initialize_for_runtime_config(&pdcpath)
.map_err(|e| LoaderError::InitFailed {
bundle: temp_path.to_string_lossy().into_owned(),
error: format!("CLR init failed: {}", e),
})?;
let _: Result<(), std::io::Error> = tmp_path_guard.close();
Ok(Arc::new(DotnetContext {
_context: Mutex::new(context),
bridge: OnceCell::new(),
}))
}
impl DotnetContext {
fn bridge(&self) -> Result<&ByteBridge, LoaderError> {
if !BYTE_BRIDGE_AVAILABLE || BYTE_BRIDGE_DLL.is_empty() {
return Err(LoaderError::InitFailed {
bundle: "<byte-bridge>".to_owned(),
error: "byte-load bridge unavailable: rebuild polyplug_dotnet with the .NET SDK installed".to_owned(),
});
}
self.bridge.get_or_try_init(|| self.init_bridge())
}
fn init_bridge(&self) -> Result<ByteBridge, LoaderError> {
let dir: tempfile::TempDir = tempfile::Builder::new()
.prefix("polyplug-dotnet-bridge")
.tempdir()
.map_err(|e: std::io::Error| LoaderError::InitFailed {
bundle: "<byte-bridge>".to_owned(),
error: format!("byte-bridge stage failed: {e}"),
})?;
let dll_path: PathBuf = dir.path().join("Polyplug.Loaders.DotnetByteBridge.dll");
std::fs::write(&dll_path, BYTE_BRIDGE_DLL).map_err(|e: std::io::Error| {
LoaderError::InitFailed {
bundle: "<byte-bridge>".to_owned(),
error: format!("byte-bridge write failed: {e}"),
}
})?;
let asm_pdc: PdCString =
PdCString::from_os_str(dll_path.as_os_str()).map_err(|_| LoaderError::InitFailed {
bundle: "<byte-bridge>".to_owned(),
error: "byte-bridge path contains embedded nul byte".to_owned(),
})?;
let loader: AssemblyDelegateLoader = {
let ctx: std::sync::MutexGuard<'_, HostfxrContext<InitializedForRuntimeConfig>> =
self._context.lock().map_err(|_| LoaderError::InitFailed {
bundle: "<byte-bridge>".to_owned(),
error: "CLR context mutex poisoned (bridge)".to_owned(),
})?;
ctx.get_delegate_loader_for_assembly(asm_pdc)
.map_err(|e| LoaderError::InitFailed {
bundle: "<byte-bridge>".to_owned(),
error: format!("byte-bridge loader init failed: {e}"),
})?
};
let type_pdc: PdCString = bridge_pdc(
"Polyplug.Loaders.DotnetByteBridge.ByteBridge, Polyplug.Loaders.DotnetByteBridge",
)?;
let load_method_pdc: PdCString = bridge_pdc("LoadAndGetInit")?;
let load_path_method_pdc: PdCString = bridge_pdc("LoadFromPathAndGetInit")?;
let preload_method_pdc: PdCString = bridge_pdc("PreloadDependency")?;
let unload_method_pdc: PdCString = bridge_pdc("Unload")?;
let is_alive_method_pdc: PdCString = bridge_pdc("IsAlcAlive")?;
let load_and_get_init: BridgeLoadAndGetInitFn = {
let f: ManagedFunction<BridgeLoadAndGetInitFn> = loader
.get_function_with_unmanaged_callers_only::<BridgeLoadAndGetInitFn>(
type_pdc.as_ref(),
load_method_pdc.as_ref(),
)
.map_err(|e| LoaderError::InitSymbolMissing {
bundle: format!("<byte-bridge>: LoadAndGetInit error={e}"),
})?;
*f
};
let load_from_path_and_get_init: BridgeLoadFromPathAndGetInitFn = {
let f: ManagedFunction<BridgeLoadFromPathAndGetInitFn> = loader
.get_function_with_unmanaged_callers_only::<BridgeLoadFromPathAndGetInitFn>(
type_pdc.as_ref(),
load_path_method_pdc.as_ref(),
)
.map_err(|e| LoaderError::InitSymbolMissing {
bundle: format!("<byte-bridge>: LoadFromPathAndGetInit error={e}"),
})?;
*f
};
let preload_dependency: BridgePreloadDependencyFn = {
let f: ManagedFunction<BridgePreloadDependencyFn> = loader
.get_function_with_unmanaged_callers_only::<BridgePreloadDependencyFn>(
type_pdc.as_ref(),
preload_method_pdc.as_ref(),
)
.map_err(|e| LoaderError::InitSymbolMissing {
bundle: format!("<byte-bridge>: PreloadDependency error={e}"),
})?;
*f
};
let unload: BridgeUnloadFn = {
let f: ManagedFunction<BridgeUnloadFn> = loader
.get_function_with_unmanaged_callers_only::<BridgeUnloadFn>(
type_pdc.as_ref(),
unload_method_pdc.as_ref(),
)
.map_err(|e| LoaderError::InitSymbolMissing {
bundle: format!("<byte-bridge>: Unload error={e}"),
})?;
*f
};
let is_alc_alive: BridgeIsAlcAliveFn = {
let f: ManagedFunction<BridgeIsAlcAliveFn> = loader
.get_function_with_unmanaged_callers_only::<BridgeIsAlcAliveFn>(
type_pdc.as_ref(),
is_alive_method_pdc.as_ref(),
)
.map_err(|e| LoaderError::InitSymbolMissing {
bundle: format!("<byte-bridge>: IsAlcAlive error={e}"),
})?;
*f
};
Ok(ByteBridge {
load_and_get_init,
load_from_path_and_get_init,
preload_dependency,
unload,
is_alc_alive,
_staged_dir: dir,
})
}
pub(crate) fn preload_dependency_bytes(
&self,
runtime_id: u64,
bundle_id: u64,
dep_bytes: &[u8],
) -> Result<(), LoaderError> {
let bridge: &ByteBridge = self.bridge()?;
let code: u32 = unsafe {
(bridge.preload_dependency)(
runtime_id,
bundle_id,
dep_bytes.as_ptr(),
dep_bytes.len() as i32,
)
};
if code != 0 {
return Err(LoaderError::InitFailed {
bundle: "<byte-bridge-dependency>".to_owned(),
error: format!("bridge PreloadDependency returned {code}"),
});
}
Ok(())
}
pub(crate) fn get_init_fn_from_bytes(
&self,
runtime_id: u64,
bundle_id: u64,
asm_name: &str,
asm_bytes: &[u8],
simple_type_name: &str,
) -> Result<InitFn, LoaderError> {
let bridge: &ByteBridge = self.bridge()?;
let type_name_bytes: &[u8] = simple_type_name.as_bytes();
let raw: *const core::ffi::c_void = unsafe {
(bridge.load_and_get_init)(
runtime_id,
bundle_id,
asm_bytes.as_ptr(),
asm_bytes.len() as i32,
type_name_bytes.as_ptr(),
type_name_bytes.len() as i32,
)
};
if raw.is_null() {
return Err(LoaderError::InitSymbolMissing {
bundle: format!(
"{asm_name}: byte-bridge could not resolve {simple_type_name}.PolyplugInit"
),
});
}
let init_fn: InitFn =
unsafe { core::mem::transmute::<*const core::ffi::c_void, InitFn>(raw) };
Ok(init_fn)
}
pub(crate) fn get_init_fn_from_path(
&self,
runtime_id: u64,
bundle_id: u64,
asm_path: &std::path::Path,
asm_name: &str,
simple_type_name: &str,
) -> Result<InitFn, LoaderError> {
let bridge: &ByteBridge = self.bridge()?;
let path_str: String = asm_path.to_string_lossy().into_owned();
let path_bytes: &[u8] = path_str.as_bytes();
let type_name_bytes: &[u8] = simple_type_name.as_bytes();
let raw: *const core::ffi::c_void = unsafe {
(bridge.load_from_path_and_get_init)(
runtime_id,
bundle_id,
path_bytes.as_ptr(),
path_bytes.len() as i32,
type_name_bytes.as_ptr(),
type_name_bytes.len() as i32,
)
};
if raw.is_null() {
return Err(LoaderError::InitSymbolMissing {
bundle: format!(
"{asm_name}: byte-bridge could not load '{path_str}' / resolve {simple_type_name}.PolyplugInit"
),
});
}
let init_fn: InitFn =
unsafe { core::mem::transmute::<*const core::ffi::c_void, InitFn>(raw) };
Ok(init_fn)
}
pub(crate) fn unload_bundle_alc(
&self,
runtime_id: u64,
bundle_id: u64,
) -> Result<(), LoaderError> {
let bridge: &ByteBridge = self.bridge()?;
let code: u32 = unsafe { (bridge.unload)(runtime_id, bundle_id) };
if code != 0 {
return Err(LoaderError::InitFailed {
bundle: format!("<byte-bridge-unload:{bundle_id}>"),
error: format!("bridge Unload returned {code}"),
});
}
Ok(())
}
pub(crate) fn is_alc_alive(
&self,
runtime_id: u64,
bundle_id: u64,
) -> Result<bool, LoaderError> {
let bridge: &ByteBridge = self.bridge()?;
let alive: u32 = unsafe { (bridge.is_alc_alive)(runtime_id, bundle_id) };
Ok(alive != 0)
}
}
fn bridge_pdc(s: &str) -> Result<PdCString, LoaderError> {
PdCString::from_os_str(std::ffi::OsStr::new(s)).map_err(|_| LoaderError::InitFailed {
bundle: "<byte-bridge>".to_owned(),
error: format!("bridge name contains embedded nul byte: {s}"),
})
}
fn find_hostfxr_auto(logger: LoggerHandle) -> Option<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
if let Some(val) = std::env::var_os("DOTNET_ROOT") {
roots.push(PathBuf::from(val));
}
let dotnet_bin: &str = if cfg!(target_os = "windows") {
"dotnet.exe"
} else {
"dotnet"
};
if let Some(path_val) = std::env::var_os("PATH") {
for dir in std::env::split_paths(&path_val) {
let candidate: PathBuf = dir.join(dotnet_bin);
if candidate.exists() {
roots.push(dir);
}
}
}
#[cfg(target_os = "windows")]
{
if let Some(program_files) = std::env::var_os("ProgramFiles") {
roots.push(PathBuf::from(program_files).join("dotnet"));
}
if let Some(program_files_x86) = std::env::var_os("ProgramFiles(x86)") {
roots.push(PathBuf::from(program_files_x86).join("dotnet"));
}
}
#[cfg(not(target_os = "windows"))]
{
roots.push(PathBuf::from("/usr/share/dotnet"));
roots.push(PathBuf::from("/usr/lib/dotnet"));
if let Some(home) = std::env::var_os("HOME") {
roots.push(PathBuf::from(home).join(".dotnet"));
}
}
for root in &roots {
if let Some(fxr_path) = highest_version_hostfxr(root, logger) {
return Some(fxr_path);
}
}
None
}
fn highest_version_hostfxr(dotnet_root: &std::path::Path, logger: LoggerHandle) -> Option<PathBuf> {
let fxr_dir: PathBuf = dotnet_root.join("host").join("fxr");
if !fxr_dir.is_dir() {
return None;
}
let mut versions: Vec<(Vec<u64>, PathBuf)> = Vec::new();
let entries: fs::ReadDir = fs::read_dir(&fxr_dir)
.map_err(|e: std::io::Error| {
logger.log(LogLevel::Error, "loader.dotnet", || {
format!(
"highest_version_hostfxr: failed to read dir '{}': {}",
fxr_dir.display(),
e
)
});
})
.ok()?;
for entry in entries.flatten() {
let path: PathBuf = entry.path();
if !path.is_dir() {
continue;
}
let name: String = path.file_name()?.to_string_lossy().into_owned();
let parts: Vec<u64> = name
.split('.')
.map(|s: &str| s.parse::<u64>().unwrap_or(0))
.collect();
if parts.is_empty() {
continue;
}
versions.push((parts, path));
}
if versions.is_empty() {
return None;
}
versions.sort_by(|a: &(Vec<u64>, PathBuf), b: &(Vec<u64>, PathBuf)| b.0.cmp(&a.0));
#[cfg(target_os = "windows")]
let lib_name: &str = "hostfxr.dll";
#[cfg(target_os = "macos")]
let lib_name: &str = "libhostfxr.dylib";
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
let lib_name: &str = "libhostfxr.so";
let best_path: PathBuf = versions[0].1.join(lib_name);
if best_path.exists() {
Some(best_path)
} else {
None
}
}