dexterous_developer_dylib_runner 0.4.0-alpha.3

A modular hot reload system for rust
Documentation
use std::sync::Arc;

use camino::Utf8Path;

use dexterous_developer_instance::{runner::HotReloadInfoBuilder, UpdatedAsset};
use dexterous_developer_types::cargo_path_utils::dylib_path;
use dylib_runner_message::DylibRunnerMessage;
use error::DylibRunnerError;
use ffi::{NEXT_LIBRARY, NEXT_UPDATE_VERSION, ORIGINAL_LIBRARY};
use remote_connection::connect_to_server;
use safer_ffi::prelude::c_slice;
use tracing::{error, trace, warn};

use dexterous_developer_instance::library_holder::LibraryHolder;

use crate::{
    dylib_runner_message::{self, DylibRunnerOutput},
    error,
    ffi::{self, OUTPUT_SENDER},
    remote_connection,
};

pub fn run_reloadable_app(
    working_directory: &Utf8Path,
    library_path: &Utf8Path,
    server: url::Url,
    in_workspace: bool,
) -> Result<(), DylibRunnerError> {
    if !library_path.exists() {
        return Err(DylibRunnerError::LibraryDirectoryDoesntExist(
            library_path.to_owned(),
        ));
    }
    if !working_directory.exists() {
        return Err(DylibRunnerError::WorkingDirectoryDoesntExist(
            working_directory.to_owned(),
        ));
    }

    let library_path = library_path.canonicalize_utf8()?;

    let dylib_paths = dylib_path();
    if !dylib_paths.contains(&library_path.to_owned()) {
        return Err(DylibRunnerError::DylibPathsMissingLibraries);
    }

    run_app(|tx, _| {
        connect_to_server(
            working_directory,
            &library_path,
            server.clone(),
            tx,
            in_workspace,
        )
    })
}

pub fn run_app<
    T: Fn(
        async_channel::Sender<DylibRunnerMessage>,
        async_channel::Receiver<DylibRunnerOutput>,
    ) -> Result<std::thread::JoinHandle<Result<(), DylibRunnerError>>, DylibRunnerError>,
>(
    connect: T,
) -> Result<(), DylibRunnerError> {
    let (tx, rx) = async_channel::unbounded::<DylibRunnerMessage>();
    let (out_tx, out_rx) = async_channel::unbounded::<DylibRunnerOutput>();

    let handle = connect(tx, out_rx)?;

    let (initial, id, path, builder_type) = {
        trace!("Getting Initial Root");
        let mut library = None;
        let mut id = None;
        #[allow(unused_assignments)]
        let mut path = None;
        #[allow(unused_assignments)]
        let mut builder_type = None;
        loop {
            if library.is_some() || id.is_some() {
                warn!("We have a root set already...");
            }
            let initial = rx.recv_blocking()?;
            trace!("Got Message While Looking For Root - {initial:?}");
            match initial {
                DylibRunnerMessage::ConnectionClosed => {
                    let _ = handle.join().map_err(DylibRunnerError::JoinHandleFailed)?;
                    return Ok(());
                }
                DylibRunnerMessage::LoadRootLib {
                    build_id,
                    local_path,
                    builder_type: bt,
                } => {
                    trace!("Loading Initial Root");
                    library = Some(LibraryHolder::new(&local_path, false, bt)?);
                    path = Some(local_path);
                    id = Some(build_id);
                    builder_type = Some(bt);
                    break;
                }
                DylibRunnerMessage::AssetUpdated { local_path, name } => {
                    trace!("Asset: {name} {local_path}");
                    continue;
                }
                DylibRunnerMessage::SerializedMessage { message: _ } => {}
            }
        }
        trace!("Initial Root ID: {id:?}");
        (
            library.ok_or(DylibRunnerError::NoInitialLibrary)?,
            id.ok_or(DylibRunnerError::NoInitialLibrary)?,
            path.ok_or(DylibRunnerError::NoInitialLibrary)?,
            builder_type.ok_or(DylibRunnerError::NoBuilderType)?,
        )
    };

    let initial = Arc::new(initial);

    NEXT_UPDATE_VERSION.store(id, std::sync::atomic::Ordering::SeqCst);
    NEXT_LIBRARY.store(Some(Arc::new(path.clone())));
    ORIGINAL_LIBRARY
        .set(initial.clone())
        .map_err(|_| DylibRunnerError::OnceCellError)?;
    OUTPUT_SENDER
        .set(Arc::new(out_tx.clone()))
        .map_err(|_| DylibRunnerError::OnceCellError)?;

    let _handle = std::thread::spawn(|| update_loop(rx, handle));

    trace!("Setting Info");

    let info = HotReloadInfoBuilder {
        internal_last_update_version: ffi::last_update_version,
        internal_update_ready: ffi::update_ready,
        internal_update: ffi::update,
        internal_validate_setup: ffi::validate_setup,
        internal_send_output: ffi::send_output,
        builder_type: safer_ffi::Vec::from(rmp_serde::to_vec(&builder_type)?),
    }
    .build();

    initial.varied_call("dexterous_developer_instance_set_hot_reload_info", info)?;
    let _ = out_tx.send_blocking(DylibRunnerOutput::LoadedLib { build_id: id });
    trace!("Calling Internal Main");
    initial.call("dexterous_developer_instance_main", &mut ())?;

    trace!("Done.");

    Ok(())
}

fn update_loop(
    rx: async_channel::Receiver<DylibRunnerMessage>,
    handle: std::thread::JoinHandle<Result<(), DylibRunnerError>>,
) -> Result<(), DylibRunnerError> {
    trace!("Starting Secondary Update Loop");
    loop {
        let message = rx.recv_blocking()?;
        match message {
            DylibRunnerMessage::ConnectionClosed => {
                let _ = handle.join().map_err(DylibRunnerError::JoinHandleFailed)?;
                error!("Connection Closed");
                return Ok(());
            }
            DylibRunnerMessage::LoadRootLib {
                build_id,
                local_path,
                ..
            } => {
                trace!("Load Root New Library {local_path}");
                NEXT_UPDATE_VERSION.store(build_id, std::sync::atomic::Ordering::SeqCst);
                trace!("Stored Build ID: {build_id}");
                NEXT_LIBRARY.store(Some(Arc::new(local_path)));
                trace!("Stored Library");
                if let Some(library) = ORIGINAL_LIBRARY.get() {
                    trace!("Running Callback");
                    let _ = library.varied_call("update_callback_internal", build_id);
                }
            }
            DylibRunnerMessage::AssetUpdated { local_path, name } => {
                if let Some(library) = ORIGINAL_LIBRARY.get() {
                    trace!("Running Callback");
                    let inner_local_path = c_slice::Box::from(
                        local_path
                            .to_string()
                            .as_bytes()
                            .iter()
                            .copied()
                            .collect::<Box<[u8]>>(),
                    );
                    let inner_name =
                        c_slice::Box::from(name.as_bytes().iter().copied().collect::<Box<[u8]>>());
                    let _ = library.varied_call(
                        "update_asset_callback_internal",
                        UpdatedAsset {
                            inner_name,
                            inner_local_path,
                        },
                    );
                }
            }
            DylibRunnerMessage::SerializedMessage { message } => {
                if let Some(library) = ORIGINAL_LIBRARY.get() {
                    trace!("Sending Message");
                    let _ = library.varied_call(
                        "send_message_to_reloaded_app",
                        safer_ffi::Vec::from(message),
                    );
                }
            }
        }
    }
}