use core::ptr::null_mut;
use alloc::{
boxed::Box,
format,
string::{String, ToString},
vec,
vec::Vec,
};
use obfstr::obfstr as s;
use windows_core::{Interface, PCWSTR};
use windows_sys::Win32::System::Variant::{VARIANT, VariantClear};
use crate::com::*;
use crate::error::{ClrError, Result};
use crate::string::ComString;
use crate::variant::create_safe_array_args;
use self::file::{read_file, validate_file};
use self::runtime::{RustClrRuntime, uuid};
mod hosting;
mod file;
mod runtime;
pub use runtime::RuntimeVersion;
#[derive(Default, Debug, Clone)]
pub struct RustClr<'a> {
runtime: RustClrRuntime<'a>,
redirect_output: bool,
patch_exit: bool,
args: Option<Vec<String>>,
}
impl<'a> RustClr<'a> {
pub fn new<T: Into<ClrSource<'a>>>(source: T) -> Result<Self> {
let buffer = match source.into() {
ClrSource::File(path) => Box::leak(read_file(path)?.into_boxed_slice()),
ClrSource::Buffer(buffer) => buffer,
};
validate_file(buffer)?;
Ok(Self {
runtime: RustClrRuntime::new(buffer),
redirect_output: false,
patch_exit: false,
args: None,
})
}
pub fn with_runtime_version(mut self, version: RuntimeVersion) -> Self {
self.runtime.runtime_version = Some(version);
self
}
pub fn with_domain(mut self, domain_name: &str) -> Self {
self.runtime.domain_name = Some(domain_name.to_string());
self
}
pub fn with_args(mut self, args: Vec<&str>) -> Self {
self.args = Some(args.iter().map(|&s| s.to_string()).collect());
self
}
pub fn with_output(mut self) -> Self {
self.redirect_output = true;
self
}
pub fn with_patch_exit(mut self) -> Self {
self.patch_exit = true;
self
}
pub fn run(&mut self) -> Result<String> {
self.runtime.prepare()?;
let domain = self.runtime.get_app_domain()?;
let assembly = domain.load_name(&self.runtime.identity_assembly)?;
let args = create_safe_array_args(
self.args.clone().unwrap_or_default()
)?;
let mscorlib = domain.get_assembly(s!("mscorlib"))?;
if self.patch_exit {
runtime::patch_exit(&mscorlib)?;
}
let output_manager = if self.redirect_output {
let mut manager = ClrOutput::new(&mscorlib);
manager.redirect()?;
Some(manager)
} else {
None
};
assembly.run(args)?;
let output = match output_manager {
Some(manager) => manager.capture()?,
None => String::new(),
};
self.runtime.unload_domain()?;
Ok(output)
}
}
impl Drop for RustClr<'_> {
fn drop(&mut self) {
if let Some(cor_runtime_host) = &self.runtime.cor_runtime_host {
cor_runtime_host.Stop();
}
}
}
pub struct ClrOutput<'a> {
string_writer: Option<VARIANT>,
mscorlib: &'a _Assembly,
}
impl<'a> ClrOutput<'a> {
pub fn new(mscorlib: &'a _Assembly) -> Self {
Self { string_writer: None, mscorlib }
}
pub fn redirect(&mut self) -> Result<()> {
let console = self.mscorlib.resolve_type(s!("System.Console"))?;
let string_writer = self.mscorlib.create_instance(s!("System.IO.StringWriter"))?;
console.invoke(
s!("SetOut"),
None,
Some(vec![string_writer]),
Invocation::Static,
)?;
console.invoke(
s!("SetError"),
None,
Some(vec![string_writer]),
Invocation::Static,
)?;
self.string_writer = Some(string_writer);
Ok(())
}
pub fn capture(&self) -> Result<String> {
let mut instance = self.string_writer
.ok_or(ClrError::Msg("No StringWriter instance found"))?;
let string_writer = self.mscorlib.resolve_type(s!("System.IO.StringWriter"))?;
let to_string = string_writer.method(s!("ToString"))?;
let result = to_string.invoke(Some(instance), None)?;
let bstr = unsafe { result.Anonymous.Anonymous.Anonymous.bstrVal };
unsafe { VariantClear(&mut instance as *mut _) };
Ok(bstr.to_string())
}
}
#[derive(Debug)]
pub struct RustClrEnv {
pub runtime_version: RuntimeVersion,
pub meta_host: ICLRMetaHost,
pub runtime_info: ICLRRuntimeInfo,
pub cor_runtime_host: ICorRuntimeHost,
pub app_domain: _AppDomain,
}
impl RustClrEnv {
pub fn new(runtime_version: Option<RuntimeVersion>) -> Result<Self> {
let meta_host = CLRCreateInstance::<ICLRMetaHost>(&CLSID_CLRMETAHOST)
.map_err(|e| ClrError::MetaHostCreationError(format!("{e}")))?;
let version_str = runtime_version.unwrap_or(RuntimeVersion::V4).to_vec();
let version = PCWSTR(version_str.as_ptr());
let runtime_info = meta_host
.GetRuntime::<ICLRRuntimeInfo>(version)
.map_err(|e| ClrError::RuntimeInfoError(format!("{e}")))?;
let cor_runtime_host = runtime_info
.GetInterface::<ICorRuntimeHost>(&CLSID_COR_RUNTIME_HOST)
.map_err(|e| ClrError::RuntimeHostError(format!("{e}")))?;
if cor_runtime_host.Start() != 0 {
return Err(ClrError::RuntimeStartError);
}
let uuid = uuid()
.to_string()
.encode_utf16()
.chain(Some(0))
.collect::<Vec<u16>>();
let app_domain = cor_runtime_host
.CreateDomain(PCWSTR(uuid.as_ptr()), null_mut())
.map_err(|_| ClrError::NoDomainAvailable)?;
Ok(Self {
runtime_version: runtime_version.unwrap_or(RuntimeVersion::V4),
meta_host,
runtime_info,
cor_runtime_host,
app_domain,
})
}
}
impl Drop for RustClrEnv {
fn drop(&mut self) {
if let Err(e) = self.cor_runtime_host.UnloadDomain(
self.app_domain
.cast::<windows_core::IUnknown>()
.map(|i| i.as_raw().cast())
.unwrap_or(null_mut()),
) {
dinvk::println!("Failed to unload AppDomain: {:?}", e);
}
self.cor_runtime_host.Stop();
}
}
pub enum Invocation {
Static,
Instance,
}
#[derive(Debug, Clone)]
pub enum ClrSource<'a> {
File(&'a str),
Buffer(&'a [u8]),
}
impl<'a> From<&'a str> for ClrSource<'a> {
fn from(file: &'a str) -> Self {
ClrSource::File(file)
}
}
impl<'a, const N: usize> From<&'a [u8; N]> for ClrSource<'a> {
fn from(buffer: &'a [u8; N]) -> Self {
ClrSource::Buffer(buffer)
}
}
impl<'a> From<&'a [u8]> for ClrSource<'a> {
fn from(buffer: &'a [u8]) -> Self {
ClrSource::Buffer(buffer)
}
}
impl<'a> From<&'a Vec<u8>> for ClrSource<'a> {
fn from(buffer: &'a Vec<u8>) -> Self {
ClrSource::Buffer(buffer.as_slice())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_domain() -> Result<()> {
let output = RustClr::new("files/RustClr/bin/Release/RustClr.exe")?
.with_domain("CustomDomain")
.with_output()
.run()?;
assert!(output.contains("[CLR] AppDomain: CustomDomain"));
Ok(())
}
#[test]
fn test_with_args() -> Result<()> {
let output = RustClr::new("files/RustClr/bin/Release/RustClr.exe")?
.with_args(vec!["rustclr"])
.with_output()
.run()?;
assert!(
output.contains("[CLR] Args:")
&& output.contains("- rustclr")
);
Ok(())
}
#[test]
fn test_without_args() -> Result<()> {
let output = RustClr::new("files/RustClr/bin/Release/RustClr.exe")?
.with_output()
.run()?;
assert!(output.contains("[CLR] No args provided"));
Ok(())
}
#[test]
fn test_with_patch_exit() -> Result<()> {
let output = RustClr::new("files/RustClr/bin/Release/RustClr.exe")?
.with_output()
.with_patch_exit()
.run()?;
assert!(output.contains("[CLR] Exit was intercepted"));
Ok(())
}
}