netcorehost 0.20.1

A Rust library for hosting the .NET Core runtime.
Documentation
use crate::{
    bindings::hostfxr::{
        PATH_LIST_SEPARATOR, hostfxr_resolve_sdk2_flags_t, hostfxr_resolve_sdk2_result_key_t,
    },
    error::{HostingError, HostingResult},
    hostfxr::{AppOrHostingResult, Hostfxr},
    pdcstring::{PdCStr, PdUChar},
};

use coreclr_hosting_shared::char_t;

use std::{cell::RefCell, io, mem::MaybeUninit, path::PathBuf, ptr, slice};

use super::UNSUPPORTED_HOST_VERSION_ERROR_CODE;

impl Hostfxr {
    /// Run an application.
    ///
    /// # Arguments
    ///  * `app_path`
    ///    path to the application to run
    ///  * `args`
    ///    command-line arguments
    ///  * `host_path`
    ///    path to the host application
    ///  * `dotnet_root`
    ///    path to the .NET Core installation root
    ///
    /// This function does not return until the application completes execution.
    /// It will shutdown CoreCLR after the application executes.
    /// If the application is successfully executed, this value will return the exit code of the application.
    /// Otherwise, it will return an error code indicating the failure.
    #[cfg_attr(
        feature = "netcore3_0",
        deprecated(note = "Use `HostfxrContext::run_app` instead"),
        allow(deprecated)
    )]
    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "netcore2_1")))]
    pub fn run_app_with_args_and_startup_info<'a, A: AsRef<PdCStr>>(
        &'a self,
        app_path: &'a PdCStr,
        args: impl IntoIterator<Item = &'a PdCStr>,
        host_path: &PdCStr,
        dotnet_root: &PdCStr,
    ) -> io::Result<AppOrHostingResult> {
        let args = [&self.dotnet_exe, app_path]
            .into_iter()
            .chain(args)
            .map(|s| s.as_ptr())
            .collect::<Vec<_>>();

        let result = unsafe {
            self.lib.hostfxr_main_startupinfo(
                args.len().try_into().unwrap(),
                args.as_ptr(),
                host_path.as_ptr(),
                dotnet_root.as_ptr(),
                app_path.as_ptr(),
            )
        }
        .unwrap_or(UNSUPPORTED_HOST_VERSION_ERROR_CODE);

        Ok(AppOrHostingResult::from(result))
    }

    /// Determine the directory location of the SDK, accounting for `global.json` and multi-level lookup policy.
    ///
    /// # Arguments
    ///  * `sdk_dir` - main directory where SDKs are located in `sdk\[version]` sub-folders.
    ///  * `working_dir` - directory where the search for `global.json` will start and proceed upwards
    ///  * `allow_prerelease` - allow resolution to return a pre-release SDK version
    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "netcore2_1")))]
    pub fn resolve_sdk(
        &self,
        sdk_dir: &PdCStr,
        working_dir: &PdCStr,
        allow_prerelease: bool,
    ) -> Result<ResolveSdkResult, HostingError> {
        let flags = if allow_prerelease {
            hostfxr_resolve_sdk2_flags_t::none
        } else {
            hostfxr_resolve_sdk2_flags_t::disallow_prerelease
        };

        // Reset the output
        let raw_result = RawResolveSdkResult::default();
        RESOLVE_SDK2_DATA.with(|sdk| *sdk.borrow_mut() = Some(raw_result));

        let result = unsafe {
            self.lib.hostfxr_resolve_sdk2(
                sdk_dir.as_ptr(),
                working_dir.as_ptr(),
                flags,
                resolve_sdk2_callback,
            )
        }
        .unwrap_or(UNSUPPORTED_HOST_VERSION_ERROR_CODE);
        HostingResult::from(result).into_result()?;

        let raw_result = RESOLVE_SDK2_DATA
            .with(|sdk| sdk.borrow_mut().take())
            .unwrap();
        Ok(ResolveSdkResult::new(raw_result))
    }

    /// Get the list of all available SDKs ordered by ascending version.
    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "netcore2_1")))]
    #[must_use]
    pub fn get_available_sdks(&self) -> Vec<PathBuf> {
        self.get_available_sdks_raw(None)
    }

    /// Get the list of all available SDKs ordered by ascending version, based on the provided `dotnet` executable.
    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "netcore2_1")))]
    #[must_use]
    pub fn get_available_sdks_with_dotnet_path(&self, dotnet_path: &PdCStr) -> Vec<PathBuf> {
        self.get_available_sdks_raw(Some(dotnet_path))
    }

    #[must_use]
    fn get_available_sdks_raw(&self, dotnet_path: Option<&PdCStr>) -> Vec<PathBuf> {
        let dotnet_path = dotnet_path.map_or_else(ptr::null, |s| s.as_ptr());
        unsafe {
            self.lib
                .hostfxr_get_available_sdks(dotnet_path, get_available_sdks_callback)
        };
        GET_AVAILABLE_SDKS_DATA
            .with(|sdks| sdks.borrow_mut().take())
            .unwrap()
    }

    /// Get the native search directories of the runtime based upon the specified app.
    ///
    /// # Arguments
    ///  * `app_path` - path to application
    #[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "netcore2_1")))]
    pub fn get_native_search_directories(
        &self,
        app_path: &PdCStr,
    ) -> Result<Vec<PathBuf>, HostingError> {
        let mut buffer = Vec::<PdUChar>::new();
        let args = [self.dotnet_exe.as_ptr(), app_path.as_ptr()];

        let mut required_buffer_size = MaybeUninit::uninit();
        unsafe {
            self.lib.hostfxr_get_native_search_directories(
                args.len().try_into().unwrap(),
                args.as_ptr(),
                buffer.as_mut_ptr().cast(),
                0,
                required_buffer_size.as_mut_ptr(),
            )
        };
        let mut required_buffer_size = unsafe { required_buffer_size.assume_init() };

        buffer.reserve(required_buffer_size.try_into().unwrap());
        let result = unsafe {
            self.lib.hostfxr_get_native_search_directories(
                args.len().try_into().unwrap(),
                args.as_ptr(),
                buffer.spare_capacity_mut().as_mut_ptr().cast(),
                buffer.spare_capacity_mut().len().try_into().unwrap(),
                &raw mut required_buffer_size,
            )
        }
        .unwrap_or(UNSUPPORTED_HOST_VERSION_ERROR_CODE);
        HostingResult::from(result).into_result()?;
        unsafe { buffer.set_len(required_buffer_size.try_into().unwrap()) };

        let mut directories = Vec::new();
        let last_start = 0;
        for i in 0..buffer.len() {
            if buffer[i] == PATH_LIST_SEPARATOR as PdUChar || buffer[i] == 0 {
                buffer[i] = 0;
                let directory = PdCStr::from_slice_with_nul(&buffer[last_start..=i]).unwrap();
                directories.push(PathBuf::from(directory.to_os_string()));
                break;
            }
        }

        Ok(directories)
    }
}

thread_local! {
    static GET_AVAILABLE_SDKS_DATA: RefCell<Option<Vec<PathBuf>>> = const { RefCell::new(None) };
    static RESOLVE_SDK2_DATA: RefCell<Option<RawResolveSdkResult>> = const { RefCell::new(None) };
}

extern "C" fn get_available_sdks_callback(sdk_count: i32, sdks_ptr: *const *const char_t) {
    GET_AVAILABLE_SDKS_DATA.with(|sdks| {
        let mut sdks_opt = sdks.borrow_mut();
        let sdks = sdks_opt.get_or_insert_with(Vec::new);

        let raw_sdks = unsafe { slice::from_raw_parts(sdks_ptr, sdk_count as usize) };
        sdks.extend(raw_sdks.iter().copied().map(|raw_sdk| {
            unsafe { PdCStr::from_str_ptr(raw_sdk) }
                .to_os_string()
                .into()
        }));
    });
}

extern "C" fn resolve_sdk2_callback(key: hostfxr_resolve_sdk2_result_key_t, value: *const char_t) {
    RESOLVE_SDK2_DATA.with(|sdks| {
        let path: PathBuf = unsafe { PdCStr::from_str_ptr(value) }.to_os_string().into();
        let mut guard = sdks.borrow_mut();
        let raw_result = guard.as_mut().unwrap();
        match key {
            hostfxr_resolve_sdk2_result_key_t::resolved_sdk_dir => {
                assert_eq!(raw_result.resolved_sdk_dir, None);
                raw_result.resolved_sdk_dir = Some(path);
            }
            hostfxr_resolve_sdk2_result_key_t::global_json_path => {
                assert_eq!(raw_result.global_json_path, None);
                raw_result.global_json_path = Some(path);
            }
            hostfxr_resolve_sdk2_result_key_t::requested_version => {
                assert_eq!(raw_result.requested_version, None);
                raw_result.requested_version = Some(path);
            }
            hostfxr_resolve_sdk2_result_key_t::global_json_state => {
                assert_eq!(raw_result.global_json_state, None);
                raw_result.global_json_state = Some(path);
            }
            _ => {
                // new key encountered
            }
        }
    });
}

#[derive(Debug, Default)]
struct RawResolveSdkResult {
    pub resolved_sdk_dir: Option<PathBuf>,
    pub global_json_path: Option<PathBuf>,
    pub requested_version: Option<PathBuf>,
    pub global_json_state: Option<PathBuf>,
}

/// Result of [`Hostfxr::resolve_sdk`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "doc-cfg", doc(cfg(feature = "netcore2_1")))]
#[must_use]
pub struct ResolveSdkResult {
    /// Path to the directory of the resolved sdk.
    pub sdk_dir: PathBuf,
    /// State of the global.json encountered during sdk resolution.
    pub global_json: GlobalJsonState,
}

/// State of global.json during sdk resolution with [`Hostfxr::resolve_sdk`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GlobalJsonState {
    /// The global.json contained invalid data, such as a malformed version number.
    InvalidData,
    /// The global.json does not contain valid json.
    InvalidJson,
    /// No global.json was found.
    NotFound,
    /// A global.json was found and was valid.
    Found(GlobalJsonInfo),
}

impl ResolveSdkResult {
    fn new(raw: RawResolveSdkResult) -> Self {
        use hostfxr_sys::hostfxr_resolve_sdk2_global_json_state;
        let global_json = match raw.global_json_state {
            None => GlobalJsonState::NotFound, // assume not found, but could also be invalid
            Some(s) => match s.to_string_lossy().as_ref() {
                hostfxr_resolve_sdk2_global_json_state::INVALID_DATA => {
                    GlobalJsonState::InvalidData
                }
                hostfxr_resolve_sdk2_global_json_state::INVALID_JSON => {
                    GlobalJsonState::InvalidJson
                }
                hostfxr_resolve_sdk2_global_json_state::VALID => {
                    GlobalJsonState::Found(GlobalJsonInfo {
                        path: raw
                            .global_json_path
                            .expect("global.json found but no path provided"),
                        requested_version: raw
                            .requested_version
                            .expect("global.json found but requested version not provided")
                            .to_string_lossy()
                            .to_string(),
                    })
                }
                _ => GlobalJsonState::NotFound,
            },
        };
        let sdk_dir = raw
            .resolved_sdk_dir
            .expect("resolve_sdk2 succeeded but no sdk_dir provided.");
        Self {
            sdk_dir,
            global_json,
        }
    }
}

/// Info about global.json if valid with [`Hostfxr::resolve_sdk`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobalJsonInfo {
    path: PathBuf,
    requested_version: String,
}