use crate::{AppTheme, Container, GodoruError, GodoruResult};
use libloading::Library;
use std::ffi::CString;
use std::mem::{MaybeUninit, transmute_copy};
use std::os::raw::{c_char, c_int, c_void};
use std::path::{Path, PathBuf};
use std::ptr;
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum EngineBackend {
Auto,
LibGodotDesktop,
GodotAndroidLibrary,
Custom,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnginePaths {
pub linuxX86_64: PathBuf,
pub windowsX86_64: PathBuf,
pub windowsX86_64ImportLibrary: PathBuf,
pub androidAar: PathBuf,
}
impl EnginePaths {
pub fn fromRoot(root: impl Into<PathBuf>) -> Self {
let root = root.into();
Self {
linuxX86_64: root.join("linux").join("x86_64").join("libgodot.so"),
windowsX86_64: root.join("windows").join("x86_64").join("libgodot.dll"),
windowsX86_64ImportLibrary: root.join("windows").join("x86_64").join("libgodot.dll.a"),
androidAar: root.join("android").join("libgodot.aar"),
}
}
pub fn resolvedDefault() -> Self {
let root = option_env!("GODORU_GODOT_DIR_RESOLVED").unwrap_or("");
Self::fromRoot(root)
}
pub fn desktopLibraryForCurrentTarget(&self) -> &Path {
if cfg!(target_os = "windows") {
&self.windowsX86_64
} else {
&self.linuxX86_64
}
}
}
impl Default for EnginePaths {
fn default() -> Self {
Self::resolvedDefault()
}
}
type GDExtensionBool = u8;
type GDExtensionInterfaceFunctionPtr = Option<unsafe extern "C" fn()>;
type GDExtensionInterfaceGetProcAddress =
Option<unsafe extern "C" fn(*const c_char) -> GDExtensionInterfaceFunctionPtr>;
type GDExtensionClassLibraryPtr = *mut c_void;
type GDExtensionMethodBindPtr = *const c_void;
type GDExtensionConstStringNamePtr = *const c_void;
type GDExtensionUninitializedStringNamePtr = *mut c_void;
type GDExtensionConstTypePtr = *const c_void;
type GDExtensionTypePtr = *mut c_void;
type GDExtensionInitializationFunction = Option<
unsafe extern "C" fn(
GDExtensionInterfaceGetProcAddress,
GDExtensionClassLibraryPtr,
*mut GDExtensionInitialization,
) -> GDExtensionBool,
>;
type CreateGodotInstance =
unsafe extern "C" fn(c_int, *mut *mut c_char, GDExtensionInitializationFunction) -> *mut c_void;
type DestroyGodotInstance = unsafe extern "C" fn(*mut c_void);
type GodotInstanceBoolMethod = unsafe extern "C" fn(*mut c_void) -> bool;
type GDExtensionInitializeCallback = Option<unsafe extern "C" fn(*mut c_void, i32)>;
type GDExtensionDeinitializeCallback = Option<unsafe extern "C" fn(*mut c_void, i32)>;
type GDExtensionInterfaceStringNameNewWithLatin1Chars =
unsafe extern "C" fn(GDExtensionUninitializedStringNamePtr, *const c_char, GDExtensionBool);
type GDExtensionInterfaceClassdbGetMethodBind = unsafe extern "C" fn(
GDExtensionConstStringNamePtr,
GDExtensionConstStringNamePtr,
i64,
) -> GDExtensionMethodBindPtr;
type GDExtensionInterfaceObjectMethodBindPtrcall = unsafe extern "C" fn(
GDExtensionMethodBindPtr,
*mut c_void,
*const GDExtensionConstTypePtr,
GDExtensionTypePtr,
);
const GODOT_INSTANCE_NO_ARG_BOOL_HASH: i64 = 2240911060;
#[repr(C)]
struct GDExtensionInitialization {
minimumInitializationLevel: i32,
userdata: *mut c_void,
initialize: GDExtensionInitializeCallback,
deinitialize: GDExtensionDeinitializeCallback,
}
#[derive(Clone)]
pub struct LibGodotDesktop {
library: Arc<Library>,
createGodotInstance: CreateGodotInstance,
destroyGodotInstance: DestroyGodotInstance,
godotInstanceStartFallback: Option<GodotInstanceBoolMethod>,
godotInstanceStopFallback: Option<GodotInstanceBoolMethod>,
godotInstanceIsStartedFallback: Option<GodotInstanceBoolMethod>,
godotInstanceIterationFallback: Option<GodotInstanceBoolMethod>,
}
impl LibGodotDesktop {
pub fn load(path: impl AsRef<Path>) -> GodoruResult<Self> {
let path = path.as_ref();
if !path.is_file() {
return Err(GodoruError::MissingEnginePath(path.to_path_buf()));
}
let library = unsafe { Library::new(path) }.map_err(|err| GodoruError::EngineLoad {
path: path.to_path_buf(),
message: err.to_string(),
})?;
let createGodotInstance = unsafe {
*library
.get::<CreateGodotInstance>(b"libgodot_create_godot_instance\0")
.map_err(|err| GodoruError::SymbolLoad {
symbol: "libgodot_create_godot_instance",
message: err.to_string(),
})?
};
let destroyGodotInstance = unsafe {
*library
.get::<DestroyGodotInstance>(b"libgodot_destroy_godot_instance\0")
.map_err(|err| GodoruError::SymbolLoad {
symbol: "libgodot_destroy_godot_instance",
message: err.to_string(),
})?
};
let godotInstanceStartFallback =
unsafe { library.get::<GodotInstanceBoolMethod>(b"_ZN13GodotInstance5startEv\0") }
.ok()
.map(|symbol| *symbol);
let godotInstanceStopFallback =
unsafe { library.get::<GodotInstanceBoolMethod>(b"_ZN13GodotInstance4stopEv\0") }
.ok()
.map(|symbol| *symbol);
let godotInstanceIsStartedFallback = unsafe {
library.get::<GodotInstanceBoolMethod>(b"_ZN13GodotInstance10is_startedEv\0")
}
.ok()
.map(|symbol| *symbol);
let godotInstanceIterationFallback =
unsafe { library.get::<GodotInstanceBoolMethod>(b"_ZN13GodotInstance9iterationEv\0") }
.ok()
.map(|symbol| *symbol);
Ok(Self {
library: Arc::new(library),
createGodotInstance,
destroyGodotInstance,
godotInstanceStartFallback,
godotInstanceStopFallback,
godotInstanceIsStartedFallback,
godotInstanceIterationFallback,
})
}
pub fn createGodotInstance(&self) -> GodoruResult<GodotInstance> {
self.createGodotInstanceWithArgs(["godoru"])
}
pub fn createGodotInstanceWithArgs<I, S>(&self, args: I) -> GodoruResult<GodotInstance>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let cArgs: Vec<CString> = args
.into_iter()
.map(|arg| {
let value = arg.as_ref().to_string();
CString::new(value.clone()).map_err(|_| GodoruError::InvalidCString(value))
})
.collect::<GodoruResult<_>>()?;
let mut argv: Vec<*mut c_char> = cArgs
.iter()
.map(|arg| arg.as_ptr() as *mut c_char)
.collect();
let raw = unsafe {
(self.createGodotInstance)(
argv.len() as c_int,
argv.as_mut_ptr(),
Some(godoruExtensionInit),
)
};
if raw.is_null() {
return Err(GodoruError::InstanceCreateFailed);
}
Ok(GodotInstance {
raw,
destroyGodotInstance: self.destroyGodotInstance,
api: GodotInstanceApi::load(
self.godotInstanceStartFallback,
self.godotInstanceStopFallback,
self.godotInstanceIsStartedFallback,
self.godotInstanceIterationFallback,
),
args: cArgs,
argv,
library: Arc::clone(&self.library),
})
}
}
pub struct GodotInstance {
raw: *mut c_void,
destroyGodotInstance: DestroyGodotInstance,
api: GodotInstanceApi,
args: Vec<CString>,
argv: Vec<*mut c_char>,
library: Arc<Library>,
}
impl GodotInstance {
pub fn raw(&self) -> *mut c_void {
self.raw
}
pub fn start(&self) -> GodoruResult<()> {
if self.api.callBool(self.raw, GodotInstanceMethod::Start)? {
Ok(())
} else {
Err(GodoruError::InstanceStartFailed)
}
}
pub fn iteration(&self) -> GodoruResult<bool> {
self.api.callBool(self.raw, GodotInstanceMethod::Iteration)
}
pub fn stop(&self) -> GodoruResult<()> {
if self.api.callBool(self.raw, GodotInstanceMethod::Stop)? {
Ok(())
} else {
Err(GodoruError::EngineMethodUnavailable("stop".to_string()))
}
}
pub fn isStarted(&self) -> bool {
self.api
.callBool(self.raw, GodotInstanceMethod::IsStarted)
.unwrap_or(false)
}
pub(crate) fn mountUi<A: Clone + 'static>(
&self,
root: &Container<A>,
theme: &AppTheme,
) -> GodoruResult<crate::godot::GodotUiTree<A>> {
crate::godot::bridge()?.mountUi(root, theme)
}
}
impl Drop for GodotInstance {
fn drop(&mut self) {
if !self.raw.is_null() {
unsafe {
(self.destroyGodotInstance)(self.raw);
}
self.raw = ptr::null_mut();
}
let _ = (&self.args, &self.argv, &self.library);
}
}
unsafe extern "C" fn godoruExtensionInit(
getProcAddress: GDExtensionInterfaceGetProcAddress,
library: GDExtensionClassLibraryPtr,
initialization: *mut GDExtensionInitialization,
) -> GDExtensionBool {
if initialization.is_null() {
return 0;
}
if let Some(functions) = unsafe { GodotApiFunctions::load(getProcAddress, library) } {
let global = GODOT_API.get_or_init(|| Mutex::new(None));
if let Ok(mut api) = global.lock() {
*api = Some(functions);
}
}
crate::godot::storeBridge(getProcAddress, library);
unsafe {
(*initialization).minimumInitializationLevel = 2;
(*initialization).userdata = ptr::null_mut();
(*initialization).initialize = Some(godoruExtensionInitialize);
(*initialization).deinitialize = Some(godoruExtensionDeinitialize);
}
1
}
unsafe extern "C" fn godoruExtensionInitialize(_userdata: *mut c_void, _level: i32) {}
unsafe extern "C" fn godoruExtensionDeinitialize(_userdata: *mut c_void, _level: i32) {}
#[derive(Clone)]
struct GodotApiFunctions {
stringNameNewWithLatin1Chars: GDExtensionInterfaceStringNameNewWithLatin1Chars,
classdbGetMethodBind: GDExtensionInterfaceClassdbGetMethodBind,
objectMethodBindPtrcall: GDExtensionInterfaceObjectMethodBindPtrcall,
}
impl GodotApiFunctions {
unsafe fn load(
getProcAddress: GDExtensionInterfaceGetProcAddress,
_library: GDExtensionClassLibraryPtr,
) -> Option<Self> {
Some(Self {
stringNameNewWithLatin1Chars: unsafe {
loadProc(getProcAddress, b"string_name_new_with_latin1_chars\0")?
},
classdbGetMethodBind: unsafe {
loadProc(getProcAddress, b"classdb_get_method_bind\0")?
},
objectMethodBindPtrcall: unsafe {
loadProc(getProcAddress, b"object_method_bind_ptrcall\0")?
},
})
}
fn methodBind(&self, method: &str) -> GodoruResult<GDExtensionMethodBindPtr> {
let className = self.stringName("GodotInstance")?;
let methodName = self.stringName(method)?;
let methodBind = unsafe {
(self.classdbGetMethodBind)(
className.as_ptr(),
methodName.as_ptr(),
GODOT_INSTANCE_NO_ARG_BOOL_HASH,
)
};
if methodBind.is_null() {
Err(GodoruError::EngineMethodUnavailable(method.to_string()))
} else {
Ok(methodBind)
}
}
fn stringName(&self, value: &str) -> GodoruResult<StringNameStorage> {
let value = CString::new(value).map_err(|_| GodoruError::InvalidCString(value.into()))?;
let mut storage = StringNameStorage::new();
unsafe {
(self.stringNameNewWithLatin1Chars)(storage.as_mut_ptr(), value.as_ptr(), 1);
}
Ok(storage)
}
}
unsafe fn loadProc<T: Copy>(
getProcAddress: GDExtensionInterfaceGetProcAddress,
name: &[u8],
) -> Option<T> {
let getProcAddress = getProcAddress?;
let function = unsafe { getProcAddress(name.as_ptr() as *const c_char) }?;
Some(unsafe { transmute_copy(&function) })
}
#[derive(Clone)]
struct GodotInstanceApi {
methodApi: Option<GodotInstanceMethodApi>,
startFallback: Option<GodotInstanceBoolMethod>,
stopFallback: Option<GodotInstanceBoolMethod>,
isStartedFallback: Option<GodotInstanceBoolMethod>,
iterationFallback: Option<GodotInstanceBoolMethod>,
}
impl GodotInstanceApi {
fn load(
startFallback: Option<GodotInstanceBoolMethod>,
stopFallback: Option<GodotInstanceBoolMethod>,
isStartedFallback: Option<GodotInstanceBoolMethod>,
iterationFallback: Option<GodotInstanceBoolMethod>,
) -> Self {
let methodApi = GODOT_API
.get()
.and_then(|api| api.lock().ok())
.and_then(|api| api.clone())
.and_then(|api| GodotInstanceMethodApi::load(api).ok());
Self {
methodApi,
startFallback,
stopFallback,
isStartedFallback,
iterationFallback,
}
}
fn callBool(&self, raw: *mut c_void, method: GodotInstanceMethod) -> GodoruResult<bool> {
if let Some(api) = &self.methodApi
&& !matches!(method, GodotInstanceMethod::Stop)
{
return api.callBool(raw, method);
}
let fallback = match method {
GodotInstanceMethod::Start => self.startFallback,
GodotInstanceMethod::Stop => self.stopFallback,
GodotInstanceMethod::IsStarted => self.isStartedFallback,
GodotInstanceMethod::Iteration => self.iterationFallback,
};
if let Some(fallback) = fallback {
Ok(unsafe { fallback(raw) })
} else {
Err(GodoruError::EngineMethodUnavailable(
method.name().to_string(),
))
}
}
}
#[derive(Clone)]
struct GodotInstanceMethodApi {
functions: GodotApiFunctions,
start: GDExtensionMethodBindPtr,
isStarted: GDExtensionMethodBindPtr,
iteration: GDExtensionMethodBindPtr,
}
impl GodotInstanceMethodApi {
fn load(functions: GodotApiFunctions) -> GodoruResult<Self> {
Ok(Self {
start: functions.methodBind("start")?,
isStarted: functions.methodBind("is_started")?,
iteration: functions.methodBind("iteration")?,
functions,
})
}
fn callBool(&self, raw: *mut c_void, method: GodotInstanceMethod) -> GodoruResult<bool> {
let methodBind = match method {
GodotInstanceMethod::Start => self.start,
GodotInstanceMethod::Stop => {
return Err(GodoruError::EngineMethodUnavailable("stop".to_string()));
}
GodotInstanceMethod::IsStarted => self.isStarted,
GodotInstanceMethod::Iteration => self.iteration,
};
let mut result = 0u8;
unsafe {
(self.functions.objectMethodBindPtrcall)(
methodBind,
raw,
ptr::null(),
&mut result as *mut u8 as GDExtensionTypePtr,
);
}
Ok(result != 0)
}
}
#[derive(Clone, Copy)]
enum GodotInstanceMethod {
Start,
Stop,
IsStarted,
Iteration,
}
impl GodotInstanceMethod {
fn name(self) -> &'static str {
match self {
GodotInstanceMethod::Start => "start",
GodotInstanceMethod::Stop => "stop",
GodotInstanceMethod::IsStarted => "is_started",
GodotInstanceMethod::Iteration => "iteration",
}
}
}
#[repr(C)]
struct StringNameStorage {
storage: MaybeUninit<[usize; 1]>,
}
impl StringNameStorage {
fn new() -> Self {
Self {
storage: MaybeUninit::uninit(),
}
}
fn as_ptr(&self) -> GDExtensionConstStringNamePtr {
self.storage.as_ptr() as GDExtensionConstStringNamePtr
}
fn as_mut_ptr(&mut self) -> GDExtensionUninitializedStringNamePtr {
self.storage.as_mut_ptr() as GDExtensionUninitializedStringNamePtr
}
}
unsafe impl Send for GodotApiFunctions {}
unsafe impl Sync for GodotApiFunctions {}
unsafe impl Send for GodotInstanceMethodApi {}
unsafe impl Sync for GodotInstanceMethodApi {}
static GODOT_API: OnceLock<Mutex<Option<GodotApiFunctions>>> = OnceLock::new();
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enginePathsFromRootBuildsKnownPaths() {
let paths = EnginePaths::fromRoot("root");
assert_eq!(
paths.linuxX86_64,
PathBuf::from("root/linux/x86_64/libgodot.so")
);
assert_eq!(
paths.windowsX86_64,
PathBuf::from("root/windows/x86_64/libgodot.dll")
);
assert_eq!(
paths.windowsX86_64ImportLibrary,
PathBuf::from("root/windows/x86_64/libgodot.dll.a")
);
assert_eq!(paths.androidAar, PathBuf::from("root/android/libgodot.aar"));
}
}