#![warn(missing_docs)]
mod assembly;
#[macro_use]
mod garbage_collector;
mod adt;
mod array;
mod dispatch_table;
mod function_info;
mod marshal;
mod reflection;
use anyhow::Result;
use dispatch_table::DispatchTable;
use garbage_collector::GarbageCollector;
use log::{debug, error, info};
use mun_abi as abi;
use mun_memory::{
gc::{self, Array, GcRuntime},
type_table::TypeTable,
};
use mun_project::LOCKFILE_NAME;
use notify::{event::ModifyKind, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::{
collections::{HashMap, VecDeque},
ffi,
ffi::c_void,
fmt::{Debug, Display, Formatter},
io,
mem::ManuallyDrop,
path::{Path, PathBuf},
ptr::NonNull,
sync::{
mpsc::{channel, Receiver},
Arc,
},
};
pub use crate::{
adt::{RootedStruct, StructRef},
array::{ArrayRef, RootedArray},
assembly::Assembly,
function_info::{
FunctionDefinition, FunctionPrototype, FunctionSignature, IntoFunctionDefinition,
},
marshal::Marshal,
reflection::{ArgumentReflection, ReturnTypeReflection},
};
use crate::array::RawArray;
pub use mun_memory::{Field, FieldData, HasStaticType, PointerType, StructType, Type};
pub struct RuntimeOptions {
pub library_path: PathBuf,
pub type_table: TypeTable,
pub user_functions: Vec<FunctionDefinition>,
}
unsafe fn get_allocator(alloc_handle: *mut ffi::c_void) -> Arc<GarbageCollector> {
Arc::from_raw(alloc_handle as *const GarbageCollector)
}
unsafe fn get_type_info(type_handle: *const ffi::c_void) -> Type {
Type::from_raw(type_handle)
}
extern "C" fn new(
type_handle: *const ffi::c_void,
alloc_handle: *mut ffi::c_void,
) -> *const *mut ffi::c_void {
let type_info = ManuallyDrop::new(unsafe { get_type_info(type_handle) });
let allocator = ManuallyDrop::new(unsafe { get_allocator(alloc_handle) });
let handle = allocator.as_ref().alloc(&type_info);
handle.into()
}
extern "C" fn new_array(
type_handle: *const ffi::c_void,
length: usize,
alloc_handle: *mut ffi::c_void,
) -> *const *mut ffi::c_void {
let type_info = ManuallyDrop::new(unsafe { get_type_info(type_handle) });
let allocator = ManuallyDrop::new(unsafe { get_allocator(alloc_handle) });
let handle = allocator.as_ref().alloc_array(&type_info, length);
handle.as_raw().into()
}
pub struct RuntimeBuilder {
options: RuntimeOptions,
}
impl RuntimeBuilder {
fn new<P: Into<PathBuf>>(library_path: P) -> Self {
Self {
options: RuntimeOptions {
library_path: library_path.into(),
type_table: Default::default(),
user_functions: Default::default(),
},
}
}
pub fn insert_fn<S: Into<String>, F: IntoFunctionDefinition>(
mut self,
name: S,
func: F,
) -> Self {
self.options.user_functions.push(func.into(name));
self
}
pub unsafe fn finish(self) -> anyhow::Result<Runtime> {
Runtime::new(self.options)
}
}
pub struct Runtime {
assemblies: HashMap<PathBuf, Assembly>,
assemblies_to_relink: VecDeque<(PathBuf, PathBuf)>,
dispatch_table: DispatchTable,
type_table: TypeTable,
watcher: RecommendedWatcher,
watcher_rx: Receiver<notify::Result<Event>>,
renamed_files: HashMap<usize, PathBuf>,
gc: Arc<GarbageCollector>,
}
impl Runtime {
pub fn builder<P: Into<PathBuf>>(library_path: P) -> RuntimeBuilder {
RuntimeBuilder::new(library_path)
}
pub unsafe fn new(mut options: RuntimeOptions) -> anyhow::Result<Runtime> {
let (tx, rx) = channel();
let mut dispatch_table = DispatchTable::default();
let type_table = options.type_table;
options.user_functions.push(IntoFunctionDefinition::into(
new as extern "C" fn(*const ffi::c_void, *mut ffi::c_void) -> *const *mut ffi::c_void,
"new",
));
options.user_functions.push(IntoFunctionDefinition::into(
new_array
as extern "C" fn(
*const ffi::c_void,
usize,
*mut ffi::c_void,
) -> *const *mut ffi::c_void,
"new_array",
));
options.user_functions.into_iter().for_each(|fn_def| {
dispatch_table.insert_fn(fn_def.prototype.name.clone(), Arc::new(fn_def));
});
let watcher: RecommendedWatcher = notify::recommended_watcher(move |res| {
tx.send(res).expect("Failed to send filesystem event.")
})?;
let mut runtime = Runtime {
assemblies: HashMap::new(),
assemblies_to_relink: VecDeque::new(),
dispatch_table,
type_table,
watcher,
watcher_rx: rx,
renamed_files: HashMap::new(),
gc: Arc::new(self::garbage_collector::GarbageCollector::default()),
};
runtime.add_assembly(&options.library_path)?;
Ok(runtime)
}
unsafe fn add_assembly(&mut self, library_path: &Path) -> anyhow::Result<()> {
let library_path = library_path.canonicalize()?;
if self.assemblies.contains_key(&library_path) {
return Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"An assembly with the same name already exists.",
)
.into());
}
let mut loaded = HashMap::new();
let mut to_load = VecDeque::new();
to_load.push_back(library_path);
while let Some(library_path) = to_load.pop_front() {
if loaded.contains_key(&library_path) {
continue;
}
let assembly = Assembly::load(&library_path, self.gc.clone())?;
let parent = library_path.parent().expect("Invalid library path");
let extension = library_path.extension();
let dependencies: Vec<String> =
assembly.info().dependencies().map(From::from).collect();
loaded.insert(library_path.clone(), assembly);
for dependency in dependencies {
let mut library_path = parent.join(dependency);
if let Some(extension) = extension {
library_path = library_path.with_extension(extension);
}
if !loaded.contains_key(&library_path) {
to_load.push_back(library_path);
}
}
}
(self.dispatch_table, self.type_table) =
Assembly::link_all(loaded.values_mut(), &self.dispatch_table, &self.type_table)?;
for (library_path, assembly) in loaded.into_iter() {
self.watcher
.watch(library_path.parent().unwrap(), RecursiveMode::NonRecursive)?;
self.assemblies.insert(library_path, assembly);
}
Ok(())
}
pub fn get_function_definition(&self, function_name: &str) -> Option<Arc<FunctionDefinition>> {
self.dispatch_table.get_fn(function_name)
}
pub fn get_type_info_by_name(&self, type_name: &str) -> Option<Type> {
self.type_table.find_type_info_by_name(type_name)
}
pub fn get_type_info_by_id(&self, type_id: &abi::TypeId) -> Option<Type> {
self.type_table.find_type_info_by_id(type_id)
}
pub unsafe fn update(&mut self) -> bool {
fn is_lockfile(path: &Path) -> bool {
path.file_name().expect("Invalid file path.") == LOCKFILE_NAME
}
unsafe fn relink_assemblies(
runtime: &mut Runtime,
) -> anyhow::Result<(DispatchTable, TypeTable)> {
let mut loaded = HashMap::new();
let to_load = &mut runtime.assemblies_to_relink;
info!("Relinking assemblies:");
for (old_path, new_path) in to_load.iter() {
info!(
"{} -> {}",
old_path.to_string_lossy(),
new_path.to_string_lossy()
);
}
while let Some((old_path, new_path)) = to_load.pop_front() {
if loaded.contains_key(&old_path) {
continue;
}
let assembly = Assembly::load(&new_path, runtime.gc.clone())?;
let parent = new_path.parent().expect("Invalid library path");
let extension = new_path.extension();
let dependencies: Vec<String> =
assembly.info().dependencies().map(From::from).collect();
loaded.insert(old_path.clone(), assembly);
for dependency in dependencies {
let mut library_path = parent.join(dependency);
if let Some(extension) = extension {
library_path = library_path.with_extension(extension);
}
if !loaded.contains_key(&library_path)
&& !runtime.assemblies.contains_key(&library_path)
{
to_load.push_back((old_path.clone(), library_path));
}
}
}
Assembly::relink_all(
&mut loaded,
&mut runtime.assemblies,
&runtime.dispatch_table,
&runtime.type_table,
)
}
let mut requires_relink = false;
while let Ok(Ok(event)) = self.watcher_rx.try_recv() {
for path in event.paths {
if is_lockfile(&path) {
match event.kind {
EventKind::Create(_) => debug!("Lockfile created"),
EventKind::Remove(_) => {
debug!("Lockfile deleted");
requires_relink = true;
}
_ => (),
}
} else {
let path = path.canonicalize().unwrap_or_else(|_| {
panic!("Failed to canonicalize path: {}.", path.to_string_lossy())
});
match event.kind {
EventKind::Modify(ModifyKind::Name(_)) => {
let tracker = event.attrs.tracker().expect("Invalid RENAME event.");
if let Some(old_path) = self.renamed_files.remove(&tracker) {
self.assemblies_to_relink.push_back((old_path, path));
} else {
self.renamed_files.insert(tracker, path);
}
}
EventKind::Modify(_) => {
self.assemblies_to_relink.push_back((path.clone(), path));
}
_ => (),
}
}
}
}
if requires_relink {
if self.assemblies_to_relink.is_empty() {
debug!("The compiler didn't write a munlib.");
} else {
match relink_assemblies(self) {
Ok((dispatch_table, type_table)) => {
info!("Succesfully reloaded assemblies.");
self.dispatch_table = dispatch_table;
self.type_table = type_table;
self.assemblies_to_relink.clear();
return true;
}
Err(e) => error!("Failed to relink assemblies, due to {}.", e),
}
}
}
false
}
pub fn gc(&self) -> &GarbageCollector {
self.gc.as_ref()
}
pub fn gc_collect(&self) -> bool {
self.gc.collect()
}
pub fn gc_stats(&self) -> gc::Stats {
self.gc.stats()
}
pub fn construct_typed_array<
't,
T: 't + Marshal<'t> + ArgumentReflection,
I: IntoIterator<Item = T>,
>(
&'t self,
element_type: &Type,
iter: I,
) -> ArrayRef<'t, T>
where
I::IntoIter: ExactSizeIterator,
{
let iter = iter.into_iter();
let array_type = element_type.array_type();
let array_capacity = iter
.size_hint()
.1
.expect("iterator doesn't return upper bound");
let mut array_handle = self.gc.alloc_array(&array_type, array_capacity);
let mut element_ptr = array_handle.data().as_ptr();
let element_stride = array_handle.element_stride();
let mut size = 0;
for (idx, element) in iter.enumerate() {
debug_assert!(
idx < array_capacity,
"looks like the size_hint was not properly implemented"
);
assert_eq!(&element.type_info(self), element_type);
T::marshal_to_ptr(
element,
unsafe { NonNull::new_unchecked(element_ptr).cast() },
element_type,
);
element_ptr = unsafe { element_ptr.add(element_stride) };
size = idx + 1;
}
unsafe {
array_handle.set_length(size);
}
ArrayRef::new(RawArray(array_handle.as_raw()), self)
}
pub fn construct_array<'t, T: 't + Marshal<'t> + HasStaticType, I: IntoIterator<Item = T>>(
&'t self,
iter: I,
) -> ArrayRef<'t, T>
where
I::IntoIter: ExactSizeIterator,
{
let iter = iter.into_iter();
let element_type = T::type_info();
let array_type = element_type.array_type();
let array_capacity = iter
.size_hint()
.1
.expect("iterator doesn't return upper bound");
let mut array_handle = self.gc.alloc_array(&array_type, array_capacity);
let mut element_ptr = array_handle.data().as_ptr();
let element_stride = array_handle.element_stride();
let mut size = 0;
for (idx, element) in iter.enumerate() {
debug_assert!(
idx < array_capacity,
"looks like the size_hint was not properly implemented"
);
T::marshal_to_ptr(
element,
unsafe { NonNull::new_unchecked(element_ptr).cast() },
element_type,
);
element_ptr = unsafe { element_ptr.add(element_stride) };
size = idx + 1;
}
unsafe {
array_handle.set_length(size);
}
ArrayRef::new(RawArray(array_handle.as_raw()), self)
}
}
pub struct InvokeErr<'name, T> {
msg: String,
function_name: &'name str,
arguments: T,
}
impl<'name, T> Debug for InvokeErr<'name, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.msg)
}
}
impl<'name, T> Display for InvokeErr<'name, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.msg)
}
}
impl<'name, T: InvokeArgs> InvokeErr<'name, T> {
pub fn retry<'r, 'o, Output>(self, runtime: &'r mut Runtime) -> Result<Output, Self>
where
Output: 'o + ReturnTypeReflection + Marshal<'o>,
'r: 'o,
{
unsafe { self.retry_impl(runtime) }
}
pub fn wait<'r, 'o, Output>(mut self, runtime: &'r mut Runtime) -> Output
where
Output: 'o + ReturnTypeReflection + Marshal<'o>,
'r: 'o,
{
let runtime = &*runtime;
loop {
self = match unsafe { self.retry_impl(runtime) } {
Ok(output) => return output,
Err(e) => e,
};
}
}
unsafe fn retry_impl<'r, 'o, Output>(self, runtime: &'r Runtime) -> Result<Output, Self>
where
Output: 'o + ReturnTypeReflection + Marshal<'o>,
'r: 'o,
{
#[allow(clippy::cast_ref_to_mut)]
let runtime = &mut *(runtime as *const Runtime as *mut Runtime);
eprintln!("{}", self.msg);
while !runtime.update() {
}
runtime.invoke(self.function_name, self.arguments)
}
}
pub trait InvokeArgs {
fn can_invoke<'runtime>(
&self,
runtime: &'runtime Runtime,
signature: &FunctionSignature,
) -> Result<(), String>;
unsafe fn invoke<ReturnType>(self, fn_ptr: *const c_void) -> ReturnType;
}
seq_macro::seq!(N in 0..=20 {#(
seq_macro::seq!(I in 0..N {
#[allow(clippy::extra_unused_lifetimes)]
impl<'arg, #(T~I: ArgumentReflection + Marshal<'arg>,)*> InvokeArgs for (#(T~I,)*) {
#[allow(unused_variables)]
fn can_invoke<'runtime>(&self, runtime: &'runtime Runtime, signature: &FunctionSignature) -> Result<(), String> {
let arg_types = &signature.arg_types;
#[allow(clippy::len_zero)]
if N != arg_types.len() {
return Err(format!("Invalid argument count. Expected {} arguments, got {}", arg_types.len(), N))
}
#(
if arg_types[I] != self.I.type_info(runtime) {
return Err(format!(
"Invalid argument type at index {}. Expected: {}. Found: {}.",
I,
self.I.type_info(runtime).name(),
arg_types[I].name(),
));
}
)*
Ok(())
}
unsafe fn invoke<ReturnType>(self, fn_ptr: *const c_void) -> ReturnType {
#[allow(clippy::type_complexity)]
let function: fn(#(T~I::MunType,)*) -> ReturnType = core::mem::transmute(fn_ptr);
function(#(self.I.marshal_into(),)*)
}
}
});
)*});
impl Runtime {
pub fn invoke<
'runtime,
'ret,
'name,
ReturnType: ReturnTypeReflection + Marshal<'ret> + 'ret,
ArgTypes: InvokeArgs,
>(
&'runtime self,
function_name: &'name str,
arguments: ArgTypes,
) -> Result<ReturnType, InvokeErr<'name, ArgTypes>>
where
'runtime: 'ret,
{
let function_info = match self.get_function_definition(function_name).ok_or_else(|| {
format!(
"failed to obtain function '{}', no such function exists.",
function_name
)
}) {
Ok(function_info) => function_info,
Err(msg) => {
return Err(InvokeErr {
msg,
function_name,
arguments,
})
}
};
match arguments.can_invoke(self, &function_info.prototype.signature) {
Ok(_) => {}
Err(msg) => {
return Err(InvokeErr {
msg,
function_name,
arguments,
})
}
};
if !ReturnType::accepts_type(&function_info.prototype.signature.return_type) {
return Err(InvokeErr {
msg: format!(
"unexpected return type, got '{}', expected '{}",
&function_info.prototype.signature.return_type.name(),
ReturnType::type_hint()
),
function_name,
arguments,
});
}
let result: ReturnType::MunType = unsafe { arguments.invoke(function_info.fn_ptr) };
Ok(Marshal::marshal_from(result, self))
}
}