use std::{ffi::CString, ptr::NonNull};
use log::Log;
use crate::{UnityInterface, ffi, unity_api_guid};
pub struct UnityLogger {
ptr: NonNull<ffi::IUnityLog>,
}
unsafe impl Send for UnityLogger {}
unsafe impl Sync for UnityLogger {}
unsafe impl UnityInterface for UnityLogger {
type FFIType = ffi::IUnityLog;
type FFIConversionError = ();
const GUID: ffi::UnityInterfaceGUID = unity_api_guid!(0x9E7507FA5B444D5D 0x92FB979515EA83FC);
}
impl TryFrom<NonNull<ffi::IUnityLog>> for UnityLogger {
type Error = ();
fn try_from(value: NonNull<ffi::IUnityLog>) -> Result<Self, Self::Error> {
Ok(Self { ptr: value })
}
}
pub enum UnityLogType {
Info,
Warning,
Error,
Exception,
}
impl From<log::Level> for UnityLogType {
fn from(value: log::Level) -> Self {
match value {
log::Level::Error => UnityLogType::Error,
log::Level::Warn => UnityLogType::Warning,
log::Level::Info => UnityLogType::Info,
log::Level::Debug => UnityLogType::Info,
log::Level::Trace => UnityLogType::Info,
}
}
}
impl From<UnityLogType> for ffi::UnityLogType {
fn from(value: UnityLogType) -> Self {
match value {
UnityLogType::Info => ffi::UnityLogType::kUnityLogTypeLog,
UnityLogType::Warning => ffi::UnityLogType::kUnityLogTypeWarning,
UnityLogType::Error => ffi::UnityLogType::kUnityLogTypeError,
UnityLogType::Exception => ffi::UnityLogType::kUnityLogTypeException,
}
}
}
impl UnityLogger {
pub fn to_rust_logger(self, initial_level: log::LevelFilter) -> UnityRustLogger {
self.make_rust_logger_internal(initial_level, None)
}
pub fn to_rust_logger_for_app(
self,
initial_level: log::LevelFilter,
app_name: &'static str,
) -> UnityRustLogger {
self.make_rust_logger_internal(initial_level, Some(app_name))
}
fn make_rust_logger_internal(
self,
initial_level: log::LevelFilter,
app_name: Option<&'static str>,
) -> UnityRustLogger {
log::set_max_level(initial_level);
UnityRustLogger {
app_prefix: app_name,
logger: self,
}
}
pub fn log_generic(&self, level: UnityLogType, msg: &str, filename: &str, line: u32) {
let line_c = std::os::raw::c_int::try_from(line).unwrap_or(std::os::raw::c_int::MIN);
let message_c_str = filter_str_to_c_string(msg);
let filename_c_str = filter_str_to_c_string(filename);
unsafe {
let logger_raw = self.ptr.as_ref();
let logfn = logger_raw.Log.unwrap();
logfn(
level.into(),
message_c_str.as_ptr(),
filename_c_str.as_ptr(),
line_c,
);
}
}
pub fn log_info(&self, msg: &str, filename: &str, line: u32) {
self.log_generic(UnityLogType::Info, msg, filename, line)
}
pub fn log_warning(&self, msg: &str, filename: &str, line: u32) {
self.log_generic(UnityLogType::Warning, msg, filename, line)
}
pub fn log_error(&self, msg: &str, filename: &str, line: u32) {
self.log_generic(UnityLogType::Error, msg, filename, line)
}
pub fn log_exception(&self, msg: &str, filename: &str, line: u32) {
self.log_generic(UnityLogType::Exception, msg, filename, line)
}
}
pub struct UnityRustLogger {
app_prefix: Option<&'static str>,
logger: UnityLogger,
}
impl Log for UnityRustLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::STATIC_MAX_LEVEL && metadata.level() <= log::max_level()
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
let needs_colons = self.app_prefix.is_some()
|| matches!(
record.level(),
log::Level::Info | log::Level::Debug | log::Level::Trace
);
let level_prefix = match record.level() {
log::Level::Error => "",
log::Level::Warn => "",
log::Level::Info => "info",
log::Level::Debug => "debug",
log::Level::Trace => "trace",
};
let app_prefix = self.app_prefix.unwrap_or("");
let colons = if needs_colons { ": " } else { "" };
let body = format!(
"{}{}{}",
format!("{} {}", app_prefix, level_prefix).trim(),
colons,
record.args()
);
self.logger.log_generic(
record.level().into(),
body.as_str(),
record.file().unwrap_or("<unknown file>"),
record.line().unwrap_or(0),
)
}
fn flush(&self) {
}
}
fn filter_str_to_c_string(s: &str) -> CString {
if !s.as_bytes().contains(&0) {
return CString::new(s).expect("No NUL bytes were detected, check invalid");
}
let mut tmp_str = String::new();
for c in s.chars() {
if c == '\0' {
tmp_str.push(char::REPLACEMENT_CHARACTER);
} else {
tmp_str.push(c);
}
}
CString::new(tmp_str).expect("Reconstructed string was invalid")
}