#include "ManagedScriptRuntime.h" #include #include #include #include #include #if MODULARITY_USE_MONO #include #include #include #include #include #endif #if MODULARITY_USE_MONO namespace { std::string trim(std::string value) { auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; while (!value.empty() && is_space(static_cast(value.front()))) { value.erase(value.begin()); } while (!value.empty() && is_space(static_cast(value.back()))) { value.pop_back(); } return value; } std::string stripAssemblyQualifier(const std::string& typeName) { auto comma = typeName.find(','); if (comma == std::string::npos) { return trim(typeName); } return trim(typeName.substr(0, comma)); } bool splitNamespaceAndName(const std::string& fullName, std::string& nameSpace, std::string& name) { auto dot = fullName.rfind('.'); if (dot == std::string::npos) { nameSpace.clear(); name = fullName; return !name.empty(); } nameSpace = fullName.substr(0, dot); name = fullName.substr(dot + 1); return !name.empty(); } std::string monoExceptionToString(MonoObject* exc) { if (!exc) return "Unknown managed exception"; MonoString* strObj = mono_object_to_string(exc, nullptr); if (!strObj) return "Managed exception (no ToString)"; char* utf8 = mono_string_to_utf8(strObj); std::string out = utf8 ? utf8 : "Managed exception (utf8 conversion failed)"; mono_free(utf8); return out; } fs::path resolveMonoRoot() { const char* env = std::getenv("MODU_MONO_ROOT"); if (env && env[0] != '\0') { return fs::path(env); } std::vector candidates; fs::path cwd = fs::current_path(); candidates.push_back(cwd / "src" / "ThirdParty" / "mono"); candidates.push_back(cwd / ".." / "src" / "ThirdParty" / "mono"); candidates.push_back(cwd / "ThirdParty" / "mono"); candidates.push_back(cwd / ".." / "ThirdParty" / "mono"); for (const auto& path : candidates) { std::error_code ec; if (fs::exists(path, ec)) { return path; } } return {}; } void configureMonoFromRoot(const fs::path& root) { fs::path libDir = root / "lib"; fs::path etcDir = root / "etc"; mono_set_dirs(libDir.string().c_str(), etcDir.string().c_str()); fs::path assembliesDir = root / "lib" / "mono" / "4.5"; if (fs::exists(assembliesDir)) { mono_set_assemblies_path(assembliesDir.string().c_str()); } mono_config_parse(nullptr); } std::string toKey(const fs::path& assemblyPath, const std::string& typeName) { return assemblyPath.lexically_normal().string() + "|" + typeName; } } // namespace struct ManagedScriptRuntime::MonoState { MonoDomain* rootDomain = nullptr; MonoDomain* scriptDomain = nullptr; fs::path monoRoot; }; ManagedScriptRuntime::~ManagedScriptRuntime() { unloadAll(); monoState.reset(); } void ManagedScriptRuntime::MonoStateDeleter::operator()(MonoState* state) const { if (!state) return; if (state->rootDomain) { if (state->scriptDomain) { mono_domain_set(state->rootDomain, false); mono_domain_unload(state->scriptDomain); state->scriptDomain = nullptr; } mono_jit_cleanup(state->rootDomain); } delete state; } bool ManagedScriptRuntime::ensureHost(const fs::path& assemblyPath) { (void)assemblyPath; if (monoState && monoState->rootDomain && monoState->scriptDomain) return true; if (!monoState) { fs::path monoRoot = resolveMonoRoot(); if (monoRoot.empty()) { lastError = "Mono root not found. Set MODU_MONO_ROOT or vendor Mono in src/ThirdParty/mono."; return false; } auto* state = new MonoState(); state->monoRoot = monoRoot; configureMonoFromRoot(monoRoot); state->rootDomain = mono_jit_init_version("Modularity", "v4.0.30319"); if (!state->rootDomain) { delete state; lastError = "Failed to initialize Mono JIT"; return false; } monoState.reset(state); std::cerr << "[Managed] Mono root: " << monoRoot << std::endl; } if (!monoState->scriptDomain) { monoState->scriptDomain = mono_domain_create_appdomain(const_cast("ModularityScripts"), nullptr); if (!monoState->scriptDomain) { lastError = "Failed to create Mono appdomain"; return false; } } mono_domain_set(monoState->scriptDomain, false); mono_thread_attach(monoState->scriptDomain); return true; } static MonoAssembly* loadAssembly(MonoDomain* domain, const fs::path& assemblyPath, MonoImage** outImage, std::string& error) { if (!outImage) { error = "Internal error: missing image output"; return nullptr; } *outImage = nullptr; std::error_code ec; if (!fs::exists(assemblyPath, ec)) { error = "Missing managed assembly: " + assemblyPath.string(); return nullptr; } mono_domain_set(domain, false); MonoAssembly* assembly = mono_domain_assembly_open(domain, assemblyPath.string().c_str()); if (!assembly) { error = "Mono failed to load assembly: " + assemblyPath.string(); return nullptr; } MonoImage* image = mono_assembly_get_image(assembly); if (!image) { error = "Mono failed to get image: " + assemblyPath.string(); return nullptr; } *outImage = image; return assembly; } bool ManagedScriptRuntime::ensureApiInjected(const fs::path& assemblyPath) { if (apiInjected) return true; if (!monoState || !monoState->scriptDomain) return false; std::string error; MonoImage* image = nullptr; MonoAssembly* assembly = loadAssembly(monoState->scriptDomain, assemblyPath, &image, error); if (!assembly || !image) { lastError = error; return false; } MonoClass* hostClass = mono_class_from_name(image, "ModuCPP", "Host"); if (!hostClass) { lastError = "Managed class ModuCPP.Host not found"; return false; } MonoMethod* setApiMethod = mono_class_get_method_from_name(hostClass, "SetNativeApi", 1); if (!setApiMethod) { lastError = "Managed method ModuCPP.Host.SetNativeApi not found"; return false; } mono_domain_set(monoState->scriptDomain, false); mono_thread_attach(monoState->scriptDomain); intptr_t apiPtr = reinterpret_cast(&api); void* args[1] = { &apiPtr }; MonoObject* exc = nullptr; mono_runtime_invoke(setApiMethod, nullptr, args, &exc); if (exc) { lastError = monoExceptionToString(exc); return false; } apiInjected = true; return true; } bool ManagedScriptRuntime::loadModuleMethods(Module& mod, const fs::path& assemblyPath, const std::string& typeName) { if (!monoState || !monoState->scriptDomain || typeName.empty()) { lastError = "Managed script type is required"; return false; } std::string error; MonoImage* image = nullptr; MonoAssembly* assembly = loadAssembly(monoState->scriptDomain, assemblyPath, &image, error); if (!assembly || !image) { lastError = error; return false; } std::string normalized = stripAssemblyQualifier(typeName); std::string nameSpace; std::string className; if (!splitNamespaceAndName(normalized, nameSpace, className)) { lastError = "Managed script type name is invalid"; return false; } MonoClass* klass = mono_class_from_name(image, nameSpace.c_str(), className.c_str()); if (!klass) { lastError = "Managed type not found: " + normalized; return false; } MonoMethod* inspector = mono_class_get_method_from_name(klass, "Script_OnInspector", 1); MonoMethod* begin = mono_class_get_method_from_name(klass, "Script_Begin", 2); MonoMethod* spec = mono_class_get_method_from_name(klass, "Script_Spec", 2); MonoMethod* testEditor = mono_class_get_method_from_name(klass, "Script_TestEditor", 2); MonoMethod* update = mono_class_get_method_from_name(klass, "Script_Update", 2); MonoMethod* tickUpdate = mono_class_get_method_from_name(klass, "Script_TickUpdate", 2); mod.inspectorMethod = inspector; mod.beginMethod = begin; mod.specMethod = spec; mod.testEditorMethod = testEditor; mod.updateMethod = update; mod.tickUpdateMethod = tickUpdate; if (!inspector && !begin && !spec && !testEditor && !update && !tickUpdate) { lastError = "No managed script exports found"; return false; } return true; } ManagedScriptRuntime::Module* ManagedScriptRuntime::getModule(const fs::path& assemblyPath, const std::string& typeName) { lastError.clear(); if (assemblyPath.empty()) { lastError = "Managed assembly path is empty"; return nullptr; } std::string key = toKey(assemblyPath, typeName); auto it = modules.find(key); if (it != modules.end()) return &it->second; if (!ensureHost(assemblyPath)) return nullptr; if (!ensureApiInjected(assemblyPath)) return nullptr; Module mod; mod.assemblyPath = assemblyPath; mod.typeName = typeName; if (!loadModuleMethods(mod, assemblyPath, typeName)) return nullptr; modules[key] = mod; return &modules[key]; } static bool invokeMonoMethod(MonoDomain* domain, MonoMethod* method, ScriptContext* ctx, float deltaTime, bool hasDelta, std::string& error) { if (!method) return false; mono_domain_set(domain, false); mono_thread_attach(domain); intptr_t ctxPtr = reinterpret_cast(ctx); void* argsWithDelta[2] = { &ctxPtr, &deltaTime }; void* argsNoDelta[1] = { &ctxPtr }; MonoObject* exc = nullptr; mono_runtime_invoke(method, nullptr, hasDelta ? argsWithDelta : argsNoDelta, &exc); if (exc) { error = monoExceptionToString(exc); return false; } return true; } bool ManagedScriptRuntime::invokeInspector(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx) { Module* mod = getModule(assemblyPath, typeName); if (!mod) return false; MonoMethod* inspector = reinterpret_cast(mod->inspectorMethod); if (!inspector) { lastError.clear(); return false; } std::string error; bool ok = invokeMonoMethod(monoState->scriptDomain, inspector, &ctx, 0.0f, false, error); if (!ok) { lastError = error; } return ok; } bool ManagedScriptRuntime::hasInspector(const fs::path& assemblyPath, const std::string& typeName) { Module* mod = getModule(assemblyPath, typeName); if (!mod) return false; if (!mod->inspectorMethod) { lastError.clear(); return false; } return true; } void ManagedScriptRuntime::tickModule(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx, float deltaTime, bool runSpec, bool runTest) { Module* mod = getModule(assemblyPath, typeName); if (!mod) return; int objId = ctx.object ? ctx.object->id : -1; MonoMethod* begin = reinterpret_cast(mod->beginMethod); if (objId >= 0 && begin && mod->beginCalledObjects.find(objId) == mod->beginCalledObjects.end()) { std::string error; if (!invokeMonoMethod(monoState->scriptDomain, begin, &ctx, deltaTime, true, error)) { lastError = error; return; } mod->beginCalledObjects.insert(objId); } MonoMethod* tickUpdate = reinterpret_cast(mod->tickUpdateMethod); MonoMethod* update = reinterpret_cast(mod->updateMethod); if (tickUpdate || update) { std::string error; if (!invokeMonoMethod(monoState->scriptDomain, tickUpdate ? tickUpdate : update, &ctx, deltaTime, true, error)) { lastError = error; return; } } if (runSpec) { MonoMethod* spec = reinterpret_cast(mod->specMethod); if (spec) { std::string error; if (!invokeMonoMethod(monoState->scriptDomain, spec, &ctx, deltaTime, true, error)) { lastError = error; return; } } } if (runTest) { MonoMethod* test = reinterpret_cast(mod->testEditorMethod); if (test) { std::string error; if (!invokeMonoMethod(monoState->scriptDomain, test, &ctx, deltaTime, true, error)) { lastError = error; return; } } } } void ManagedScriptRuntime::unloadAll() { modules.clear(); apiInjected = false; lastError.clear(); if (monoState && monoState->rootDomain && monoState->scriptDomain) { mono_domain_set(monoState->rootDomain, false); mono_domain_unload(monoState->scriptDomain); monoState->scriptDomain = nullptr; } } #else ManagedScriptRuntime::~ManagedScriptRuntime() = default; struct ManagedScriptRuntime::MonoState {}; void ManagedScriptRuntime::MonoStateDeleter::operator()(MonoState* state) const { delete state; } bool ManagedScriptRuntime::hasInspector(const fs::path& assemblyPath, const std::string& typeName) { (void)assemblyPath; (void)typeName; lastError = "Managed scripts disabled (Mono not built)."; return false; } bool ManagedScriptRuntime::invokeInspector(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx) { (void)assemblyPath; (void)typeName; (void)ctx; lastError = "Managed scripts disabled (Mono not built)."; return false; } void ManagedScriptRuntime::tickModule(const fs::path& assemblyPath, const std::string& typeName, ScriptContext& ctx, float deltaTime, bool runSpec, bool runTest) { (void)assemblyPath; (void)typeName; (void)ctx; (void)deltaTime; (void)runSpec; (void)runTest; lastError = "Managed scripts disabled (Mono not built)."; } void ManagedScriptRuntime::unloadAll() { modules.clear(); monoState.reset(); apiInjected = false; lastError.clear(); } #endif