Files
FlaxEngine/Source/Engine/Scripting/Scripting.cpp
Ari Vuollet 9d8105e3f3 Separate managed assembly unloading and scripting ALC reinitialization
Fixes an issue with multiple managed assemblies unloading and releasing
all cached data before native resources were fully released in other
assemblies.
2023-08-08 17:39:31 +03:00

1105 lines
35 KiB
C++

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#include "BinaryModule.h"
#include "Scripting.h"
#include "ScriptingType.h"
#include "FlaxEngine.Gen.h"
#include "Engine/Threading/Threading.h"
#include "Engine/Threading/ThreadLocal.h"
#include "Engine/Threading/IRunnable.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Debug/DebugLog.h"
#if USE_EDITOR
#include "Engine/Level/Level.h"
#include "Editor/Scripting/ScriptsBuilder.h"
#endif
#include "ManagedCLR/MAssembly.h"
#include "ManagedCLR/MClass.h"
#include "ManagedCLR/MMethod.h"
#include "ManagedCLR/MDomain.h"
#include "ManagedCLR/MCore.h"
#include "ManagedCLR/MException.h"
#include "Internal/StdTypesContainer.h"
#include "Engine/Core/ObjectsRemovalService.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Content/Asset.h"
#include "Engine/Content/Content.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Platform/MemoryStats.h"
#include "Engine/Serialization/JsonTools.h"
extern void registerFlaxEngineInternalCalls();
class ScriptingService : public EngineService
{
public:
ScriptingService()
: EngineService(TEXT("Scripting"), -20)
{
}
bool Init() override;
void Update() override;
void LateUpdate() override;
void FixedUpdate() override;
void LateFixedUpdate() override;
void Draw() override;
void BeforeExit() override;
void Dispose() override;
};
namespace
{
MDomain* _rootDomain = nullptr;
MDomain* _scriptsDomain = nullptr;
CriticalSection _objectsLocker;
#define USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING 0
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
struct ScriptingObjectData
{
ScriptingObject* Ptr;
StringAnsi TypeName;
ScriptingObjectData()
{
Ptr = nullptr;
}
ScriptingObjectData(ScriptingObject* ptr)
{
Ptr = ptr;
if (ptr && ptr->GetTypeHandle() && ptr->GetTypeHandle().TypeIndex < ptr->GetTypeHandle().Module->Types.Count())
TypeName = ptr->GetType().Fullname;
}
ScriptingObject* operator->() const
{
return Ptr;
}
explicit operator ScriptingObject*()
{
return Ptr;
}
operator ScriptingObject*() const
{
return Ptr;
}
};
Dictionary<Guid, ScriptingObjectData> _objectsDictionary(1024 * 16);
#else
Dictionary<Guid, ScriptingObject*> _objectsDictionary(1024 * 16);
#endif
bool _isEngineAssemblyLoaded = false;
bool _hasGameModulesLoaded = false;
MMethod* _method_Update = nullptr;
MMethod* _method_LateUpdate = nullptr;
MMethod* _method_FixedUpdate = nullptr;
MMethod* _method_LateFixedUpdate = nullptr;
MMethod* _method_Draw = nullptr;
MMethod* _method_Exit = nullptr;
Array<BinaryModule*, InlinedAllocation<64>> _nonNativeModules;
#if USE_EDITOR
bool LastBinariesLoadTriggeredCompilation = false;
#endif
}
Delegate<BinaryModule*> Scripting::BinaryModuleLoaded;
Action Scripting::ScriptsLoaded;
Action Scripting::ScriptsUnload;
Action Scripting::ScriptsReloading;
Action Scripting::ScriptsReloaded;
ThreadLocal<Scripting::IdsMappingTable*, PLATFORM_THREADS_LIMIT, true> Scripting::ObjectsLookupIdMapping;
ScriptingService ScriptingServiceInstance;
bool initFlaxEngine();
// Assembly events
void onEngineLoaded(MAssembly* assembly);
void onEngineUnloading(MAssembly* assembly);
bool ScriptingService::Init()
{
const auto startTime = DateTime::NowUTC();
// Link for assemblies events
auto engineAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly;
engineAssembly->Loaded.Bind(onEngineLoaded);
engineAssembly->Unloading.Bind(onEngineUnloading);
// Initialize managed runtime
if (MCore::LoadEngine())
{
LOG(Fatal, "C# runtime initialization failed.");
return true;
}
// Cache root domain
_rootDomain = MCore::GetRootDomain();
#if USE_SCRIPTING_SINGLE_DOMAIN
// Use single root domain
auto domain = _rootDomain;
#else
// Create Mono domain for scripts
auto domain = MCore::CreateDomain("Scripts Domain");
#endif
domain->SetCurrentDomain(true);
_scriptsDomain = domain;
// Add internal calls
registerFlaxEngineInternalCalls();
// Load assemblies
if (Scripting::Load())
{
LOG(Fatal, "Scripting Engine initialization failed.");
return true;
}
auto endTime = DateTime::NowUTC();
LOG(Info, "Scripting Engine initializated! (time: {0}ms)", (int32)((endTime - startTime).GetTotalMilliseconds()));
return false;
}
#if COMPILE_WITHOUT_CSHARP
#define INVOKE_EVENT(name)
#else
#define INVOKE_EVENT(name) \
if (!_isEngineAssemblyLoaded) return; \
if (_method_##name == nullptr) \
{ \
MClass* mclass = Scripting::GetStaticClass(); \
if (mclass == nullptr) \
{ \
LOG(Fatal, "Missing Scripting class."); \
return; \
} \
_method_##name = mclass->GetMethod("Internal_" #name, 0); \
if (_method_##name == nullptr) \
{ \
LOG(Fatal, "Missing Scripting method " #name "."); \
return; \
} \
} \
MObject* exception = nullptr; \
_method_##name->Invoke(nullptr, nullptr, &exception); \
DebugLog::LogException(exception)
#endif
void ScriptingService::Update()
{
PROFILE_CPU_NAMED("Scripting::Update");
INVOKE_EVENT(Update);
}
void ScriptingService::LateUpdate()
{
PROFILE_CPU_NAMED("Scripting::LateUpdate");
INVOKE_EVENT(LateUpdate);
}
void ScriptingService::FixedUpdate()
{
PROFILE_CPU_NAMED("Scripting::FixedUpdate");
INVOKE_EVENT(FixedUpdate);
}
void ScriptingService::LateFixedUpdate()
{
PROFILE_CPU_NAMED("Scripting::LateFixedUpdate");
INVOKE_EVENT(LateFixedUpdate);
}
void ScriptingService::Draw()
{
PROFILE_CPU_NAMED("Scripting::Draw");
INVOKE_EVENT(Draw);
}
void ScriptingService::BeforeExit()
{
PROFILE_CPU_NAMED("Scripting::BeforeExit");
INVOKE_EVENT(Exit);
}
MDomain* Scripting::GetRootDomain()
{
return _rootDomain;
}
MDomain* Scripting::GetScriptsDomain()
{
return _scriptsDomain;
}
void Scripting::ProcessBuildInfoPath(String& path, const String& projectFolderPath)
{
if (path.IsEmpty())
return;
if (path.StartsWith(TEXT("$(EnginePath)")))
path = Globals::StartupFolder / path.Substring(14);
else if (path.StartsWith(TEXT("$(ProjectPath)")))
path = projectFolderPath / path.Substring(14);
else if (FileSystem::IsRelative(path))
path = projectFolderPath / path;
}
bool Scripting::LoadBinaryModules(const String& path, const String& projectFolderPath)
{
PROFILE_CPU_NAMED("LoadBinaryModules");
LOG(Info, "Loading binary modules from build info file {0}", path);
// Read file contents
Array<byte> fileData;
if (File::ReadAllBytes(path, fileData))
{
LOG(Error, "Failed to read file contents.");
return true;
}
// Parse Json data
rapidjson_flax::Document document;
{
PROFILE_CPU_NAMED("Json.Parse");
document.Parse((char*)fileData.Get(), fileData.Count());
}
if (document.HasParseError())
{
LOG(Error, "Failed to file contents.");
return true;
}
// TODO: validate Name, Platform, Architecture, Configuration from file
// Load all references
auto referencesMember = document.FindMember("References");
if (referencesMember != document.MemberEnd() && referencesMember->value.IsArray())
{
auto& referencesArray = referencesMember->value;
for (rapidjson::SizeType i = 0; i < referencesArray.Size(); i++)
{
auto& reference = referencesArray[i];
String referenceProjectPath = JsonTools::GetString(reference, "ProjectPath", String::Empty);
if (referenceProjectPath == TEXT("$(EnginePath)/Flax.flaxproj"))
continue; // Skip reference to engine
String referencePath = JsonTools::GetString(reference, "Path", String::Empty);
if (referenceProjectPath.IsEmpty() || referencePath.IsEmpty())
{
LOG(Error, "Empty reference.");
return true;
}
ProcessBuildInfoPath(referenceProjectPath, projectFolderPath);
ProcessBuildInfoPath(referencePath, projectFolderPath);
String referenceProjectFolderPath = StringUtils::GetDirectoryName(referenceProjectPath);
if (LoadBinaryModules(referencePath, referenceProjectFolderPath))
{
LOG(Error, "Failed to load reference.");
return true;
}
}
}
// Load all binary modules
auto binaryModulesMember = document.FindMember("BinaryModules");
if (binaryModulesMember != document.MemberEnd() && binaryModulesMember->value.IsArray())
{
auto& binaryModulesArray = binaryModulesMember->value;
for (rapidjson::SizeType i = 0; i < binaryModulesArray.Size(); i++)
{
auto& binaryModule = binaryModulesArray[i];
const auto nameMember = binaryModule.FindMember("Name");
if (nameMember == binaryModule.MemberEnd())
{
LOG(Error, "Failed to process file.");
return true;
}
String name = nameMember->value.GetText();
StringAnsi nameAnsi(nameMember->value.GetString(), nameMember->value.GetStringLength());
String nativePath = JsonTools::GetString(binaryModule, "NativePath", String::Empty);
String managedPath = JsonTools::GetString(binaryModule, "ManagedPath", String::Empty);
ProcessBuildInfoPath(nativePath, projectFolderPath);
ProcessBuildInfoPath(managedPath, projectFolderPath);
LOG(Info, "Loading binary module {0}", name);
// Check if that module has been already registered
BinaryModule* module = BinaryModule::GetModule(nameAnsi);
if (!module)
{
// C++
if (nativePath.HasChars())
{
// Check if this module has been statically linked
auto& staticallyLinkedBinaryModules = StaticallyLinkedBinaryModuleInitializer::GetStaticallyLinkedBinaryModules();
for (auto getter : staticallyLinkedBinaryModules)
{
module = getter();
if (module && module->GetName() == nameAnsi)
break;
module = nullptr;
}
if (!module)
{
// Load library
const auto startTime = DateTime::NowUTC();
#if PLATFORM_ANDROID || PLATFORM_MAC
// On some platforms all native binaries are side-by-side with the app in a different folder
if (!FileSystem::FileExists(nativePath))
{
nativePath = String(StringUtils::GetDirectoryName(Platform::GetExecutableFilePath())) / StringUtils::GetFileName(nativePath);
}
#elif PLATFORM_IOS
// iOS uses Frameworks folder with native binaries
if (!FileSystem::FileExists(nativePath))
{
nativePath = Globals::ProjectFolder / TEXT("Frameworks") / StringUtils::GetFileName(nativePath);
}
#endif
auto library = Platform::LoadLibrary(nativePath.Get());
if (!library)
{
LOG(Error, "Failed to load library '{0}' for binary module {1}.", nativePath, name);
return true;
}
char getBinaryFuncName[512];
StringAnsiView getBinaryFuncNamePrefix("GetBinaryModule");
ASSERT(getBinaryFuncNamePrefix.Length() + nameAnsi.Length() < ARRAY_COUNT(getBinaryFuncName));
Platform::MemoryCopy(getBinaryFuncName, getBinaryFuncNamePrefix.Get(), getBinaryFuncNamePrefix.Length());
Platform::MemoryCopy(getBinaryFuncName + getBinaryFuncNamePrefix.Length(), nameAnsi.Get(), nameAnsi.Length());
*(getBinaryFuncName + getBinaryFuncNamePrefix.Length() + nameAnsi.Length()) = '\0';
const auto getBinaryFunc = (GetBinaryModuleFunc)Platform::GetProcAddress(library, getBinaryFuncName);
if (!getBinaryFunc)
{
Platform::FreeLibrary(library);
LOG(Error, "Failed to setup library '{0}' for binary module {1}.", nativePath, name);
return true;
}
const auto endTime = DateTime::NowUTC();
LOG(Info, "Module {0} loaded in {1}ms", name, (int32)(endTime - startTime).GetTotalMilliseconds());
// Get the binary module
module = getBinaryFunc();
if (!module)
{
Platform::FreeLibrary(library);
LOG(Error, "Failed to get binary module {0}.", name);
return true;
}
((NativeBinaryModule*)module)->Library = library;
}
}
else
{
// Create module if native library is not used
module = New<ManagedBinaryModule>(nameAnsi);
_nonNativeModules.Add(module);
}
}
#if !COMPILE_WITHOUT_CSHARP
// C#
if (managedPath.HasChars() && !((ManagedBinaryModule*)module)->Assembly->IsLoaded())
{
if (((ManagedBinaryModule*)module)->Assembly->Load(managedPath, nativePath))
{
LOG(Error, "Failed to load C# assembly '{0}' for binary module {1}.", managedPath, name);
return true;
}
}
#endif
BinaryModuleLoaded(module);
}
}
return false;
}
bool Scripting::Load()
{
PROFILE_CPU();
// Note: this action can be called from main thread (due to Mono problems with assemblies actions from other threads)
ASSERT(IsInMainThread());
#if USE_CSHARP
// Load C# core assembly
ManagedBinaryModule* corlib = GetBinaryModuleCorlib();
if (corlib->Assembly->LoadCorlib())
{
LOG(Error, "Failed to load corlib C# assembly.");
return true;
}
// Initialize C# corelib types
{
const auto& corlibClasses = corlib->Assembly->GetClasses();
bool gotAll = true;
#define CACHE_CORLIB_CLASS(var, name) gotAll &= corlibClasses.TryGet(StringAnsiView(name), MCore::TypeCache::var)
CACHE_CORLIB_CLASS(Void, "System.Void");
CACHE_CORLIB_CLASS(Object, "System.Object");
CACHE_CORLIB_CLASS(Byte, "System.Byte");
CACHE_CORLIB_CLASS(Boolean, "System.Boolean");
CACHE_CORLIB_CLASS(SByte, "System.SByte");
CACHE_CORLIB_CLASS(Char, "System.Char");
CACHE_CORLIB_CLASS(Int16, "System.Int16");
CACHE_CORLIB_CLASS(UInt16, "System.UInt16");
CACHE_CORLIB_CLASS(Int32, "System.Int32");
CACHE_CORLIB_CLASS(UInt32, "System.UInt32");
CACHE_CORLIB_CLASS(Int64, "System.Int64");
CACHE_CORLIB_CLASS(UInt64, "System.UInt64");
CACHE_CORLIB_CLASS(IntPtr, "System.IntPtr");
CACHE_CORLIB_CLASS(UIntPtr, "System.UIntPtr");
CACHE_CORLIB_CLASS(Single, "System.Single");
CACHE_CORLIB_CLASS(Double, "System.Double");
CACHE_CORLIB_CLASS(String, "System.String");
#undef CACHE_CORLIB_CLASS
if (!gotAll)
{
LOG(Error, "Failed to load corlib C# assembly.");
for (const auto& e : corlibClasses)
LOG(Info, "Class: {0}", String(e.Value->GetFullName()));
return true;
}
}
#endif
// Load FlaxEngine
const String flaxEnginePath = Globals::BinariesFolder / TEXT("FlaxEngine.CSharp.dll");
auto* flaxEngineModule = (NativeBinaryModule*)GetBinaryModuleFlaxEngine();
if (flaxEngineModule->Assembly->Load(flaxEnginePath))
{
LOG(Error, "Failed to load FlaxEngine C# assembly.");
return true;
}
// Insert type aliases for vector types that don't exist in C++ but are just typedef (properly redirect them to actual types)
// TODO: add support for automatic typedef aliases setup for scripting module to properly lookup type from the alias typename
#if USE_LARGE_WORLDS
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double2"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double3"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Double4"];
#else
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float2"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float3"];
flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Float4"];
#endif
flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector2")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector2"];
flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector3")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector3"];
flaxEngineModule->ClassToTypeIndex[flaxEngineModule->Assembly->GetClass("FlaxEngine.Vector4")] = flaxEngineModule->TypeNameToTypeIndex["FlaxEngine.Vector4"];
#if USE_EDITOR
// Skip loading game modules in Editor on startup - Editor loads them later during splash screen (eg. after first compilation)
static bool SkipFirstLoad = true;
if (SkipFirstLoad)
{
SkipFirstLoad = false;
return false;
}
// Flax.Build outputs the <target>.Build.json with binary modules to use for game scripting
const Char *target, *platform, *architecture, *configuration;
ScriptsBuilder::GetBinariesConfiguration(target, platform, architecture, configuration);
if (StringUtils::Length(target) == 0)
{
LOG(Info, "Missing EditorTarget in project. Not using game script modules.");
_hasGameModulesLoaded = true;
return false;
}
const String targetBuildInfo = Globals::ProjectFolder / TEXT("Binaries") / target / platform / architecture / configuration / target + TEXT(".Build.json");
// Call compilation if game target build info is missing
if (!FileSystem::FileExists(targetBuildInfo))
{
LOG(Info, "Missing target build info ({0})", targetBuildInfo);
if (LastBinariesLoadTriggeredCompilation)
return false;
LastBinariesLoadTriggeredCompilation = true;
ScriptsBuilder::Compile();
return false;
}
#else
const String targetBuildInfo = Globals::BinariesFolder / TEXT("Game.Build.json");
#endif
// Load game binary modules
if (LoadBinaryModules(targetBuildInfo, Globals::ProjectFolder))
{
LOG(Error, "Failed to load Game assemblies.");
return true;
}
_hasGameModulesLoaded = true;
// End
ScriptsLoaded();
return false;
}
void Scripting::Release()
{
PROFILE_CPU();
// Note: this action can be called from main thread (due to Mono problems with assemblies actions from other threads)
ASSERT(IsInMainThread());
// Fire event
ScriptsUnload();
// Cleanup
ObjectsRemovalService::Flush();
// Cleanup some managed objects
MCore::GC::Collect();
MCore::GC::WaitForPendingFinalizers();
// Release managed objects instances for persistent objects (assets etc.)
_objectsLocker.Lock();
{
for (auto i = _objectsDictionary.Begin(); i.IsNotEnd(); ++i)
{
auto obj = i->Value;
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[OnScriptingDispose] obj = 0x{0:x}, {1}", (uint64)obj.Ptr, String(obj.TypeName));
#endif
obj->OnScriptingDispose();
}
}
_objectsLocker.Unlock();
// Release assets sourced from game assemblies
const auto flaxModule = GetBinaryModuleFlaxEngine();
for (auto asset : Content::GetAssets())
{
if (asset->GetTypeHandle().Module == flaxModule)
continue;
asset->DeleteObjectNow();
}
// Unload assemblies (from back to front)
{
LOG(Info, "Unloading binary modules");
auto modules = BinaryModule::GetModules();
for (int32 i = modules.Count() - 1; i >= 0; i--)
{
auto module = modules[i];
module->Destroy(false);
}
_nonNativeModules.ClearDelete();
_hasGameModulesLoaded = false;
}
// Cleanup
MCore::GC::Collect();
MCore::GC::WaitForPendingFinalizers();
// Flush objects
ObjectsRemovalService::Flush();
// Switch domain
auto rootDomain = MCore::GetRootDomain();
if (rootDomain)
{
if (!rootDomain->SetCurrentDomain(false))
{
LOG(Error, "Failed to set current domain to root");
}
}
#if !USE_SCRIPTING_SINGLE_DOMAIN
MCore::UnloadDomain("Scripts Domain");
#endif
}
#if USE_EDITOR
void Scripting::Reload(bool canTriggerSceneReload)
{
// By default we allow to call it only from the main thread and when no scene is loaded.
// Otherwise call scene manager to perform clear scripts reload.
// It will call this method back on main thread without scenes loaded, see SceneActionType::ReloadScripts.
if (!IsInMainThread() || Level::IsAnySceneLoaded())
{
if (canTriggerSceneReload)
{
// Call scene to reload scripts
Level::ReloadScriptsAsync();
}
else
{
LOG(Warning, "Cannot reload scene on scripting reload. Flag is not set.");
}
return;
}
PROFILE_CPU();
// Ideally we would call Release and Load but this would also reload Editor objects which we want avoid
// Editor is also referencing assets and other managed objects so we should force reload everything.
// However Reload is called when no scene is loaded.
// Faster path - if no game assembly loaded yet
if (!_hasGameModulesLoaded)
{
// Just load missing assemblies
Load();
return;
}
LOG(Info, "Start user scripts reload");
ScriptsReloading();
// Flush cache (some objects may be deleted after reload start event)
ObjectsRemovalService::Flush();
// Give GC a try to cleanup old user objects and the other mess
MCore::GC::Collect();
MCore::GC::WaitForPendingFinalizers();
// Destroy objects from game assemblies (eg. not released objects that might crash if persist in memory after reload)
const auto flaxModule = GetBinaryModuleFlaxEngine();
_objectsLocker.Lock();
{
for (auto i = _objectsDictionary.Begin(); i.IsNotEnd(); ++i)
{
auto obj = i->Value;
if (obj->GetTypeHandle().Module == flaxModule)
continue;
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[OnScriptingDispose] obj = 0x{0:x}, {1}", (uint64)obj.Ptr, String(obj.TypeName));
#endif
obj->OnScriptingDispose();
}
}
_objectsLocker.Unlock();
// Release assets sourced from game assemblies
for (auto asset : Content::GetAssets())
{
if (asset->GetTypeHandle().Module == flaxModule)
continue;
asset->DeleteObjectNow();
}
// Unload all game modules
LOG(Info, "Unloading game binary modules");
auto modules = BinaryModule::GetModules();
for (int32 i = modules.Count() - 1; i >= 0; i--)
{
BinaryModule* module = modules[i];
if (module == GetBinaryModuleCorlib() || module == GetBinaryModuleFlaxEngine())
continue;
module->Destroy(true);
}
modules.Clear();
_nonNativeModules.ClearDelete();
_hasGameModulesLoaded = false;
// Release and create a new assembly load context for user assemblies
MCore::ReloadScriptingAssemblyLoadContext();
// Give GC a try to cleanup old user objects and the other mess
MCore::GC::Collect();
MCore::GC::WaitForPendingFinalizers();
// Load all game modules
if (Load())
{
LOG(Error, "User assemblies reload failed.");
}
ScriptsReloaded();
LOG(Info, "End user scripts reload");
}
#endif
MClass* Scripting::FindClass(const StringAnsiView& fullname)
{
if (fullname.IsEmpty())
return nullptr;
PROFILE_CPU();
auto& modules = BinaryModule::GetModules();
for (auto module : modules)
{
auto managedModule = dynamic_cast<ManagedBinaryModule*>(module);
if (managedModule && managedModule->Assembly->IsLoaded())
{
MClass* result = managedModule->Assembly->GetClass(fullname);
if (result != nullptr)
return result;
}
}
return nullptr;
}
ScriptingTypeHandle Scripting::FindScriptingType(const StringAnsiView& fullname)
{
if (fullname.IsEmpty())
return ScriptingTypeHandle();
PROFILE_CPU();
auto& modules = BinaryModule::GetModules();
for (auto module : modules)
{
int32 typeIndex;
if (module->FindScriptingType(fullname, typeIndex))
{
return ScriptingTypeHandle(module, typeIndex);
}
}
return ScriptingTypeHandle();
}
ScriptingObject* Scripting::NewObject(const ScriptingTypeHandle& type)
{
if (!type)
{
LOG(Error, "Invalid type.");
return nullptr;
}
const ScriptingType& scriptingType = type.GetType();
// Create unmanaged object
const ScriptingObjectSpawnParams params(Guid::New(), type);
ScriptingObject* obj = scriptingType.Script.Spawn(params);
if (obj == nullptr)
LOG(Error, "Failed to spawn object of type \'{0}\'.", scriptingType.ToString());
return obj;
}
ScriptingObject* Scripting::NewObject(const MClass* type)
{
if (type == nullptr)
{
LOG(Error, "Invalid type.");
return nullptr;
}
#if USE_CSHARP
// Get the assembly with that class
auto module = ManagedBinaryModule::FindModule(type);
if (module == nullptr)
{
LOG(Error, "Cannot find scripting assembly for type \'{0}\'.", String(type->GetFullName()));
return nullptr;
}
// Try to find the scripting type for this class
int32 typeIndex;
if (!module->ClassToTypeIndex.TryGet(type, typeIndex))
{
LOG(Error, "Cannot spawn objects of type \'{0}\'.", String(type->GetFullName()));
return nullptr;
}
const ScriptingType& scriptingType = module->Types[typeIndex];
// Create unmanaged object
const ScriptingObjectSpawnParams params(Guid::New(), ScriptingTypeHandle(module, typeIndex));
ScriptingObject* obj = scriptingType.Script.Spawn(params);
if (obj == nullptr)
LOG(Error, "Failed to spawn object of type \'{0}\'.", scriptingType.ToString());
return obj;
#else
LOG(Error, "Not supported object creation from Managed class.");
return nullptr;
#endif
}
FLAXENGINE_API ScriptingObject* FindObject(const Guid& id, MClass* type)
{
return Scripting::FindObject(id, type);
}
ScriptingObject* Scripting::FindObject(Guid id, MClass* type)
{
if (!id.IsValid())
return nullptr;
PROFILE_CPU();
// Try to map object id
const auto idsMapping = ObjectsLookupIdMapping.Get();
if (idsMapping)
{
idsMapping->TryGet(id, id);
}
// Try to find it
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
ScriptingObjectData data;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, data);
_objectsLocker.Unlock();
auto result = data.Ptr;
#else
ScriptingObject* result = nullptr;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, result);
_objectsLocker.Unlock();
#endif
if (result)
{
// Check type
if (!type || result->Is(type))
return result;
LOG(Warning, "Found scripting object with ID={0} of type {1} that doesn't match type {2}.", id, String(result->GetType().Fullname), String(type->GetFullName()));
return nullptr;
}
// Check if object can be an asset and try to load it
if (!type)
{
result = Content::LoadAsync<Asset>(id);
if (!result)
LOG(Warning, "Unable to find scripting object with ID={0}", id);
return result;
}
if (type == ScriptingObject::GetStaticClass() || type->IsSubClassOf(Asset::GetStaticClass()))
{
Asset* asset = Content::LoadAsync(id, type);
if (asset)
return asset;
}
LOG(Warning, "Unable to find scripting object with ID={0}. Required type {1}.", id, String(type->GetFullName()));
return nullptr;
}
ScriptingObject* Scripting::TryFindObject(Guid id, MClass* type)
{
if (!id.IsValid())
return nullptr;
PROFILE_CPU();
// Try to map object id
const auto idsMapping = ObjectsLookupIdMapping.Get();
if (idsMapping)
{
idsMapping->TryGet(id, id);
}
// Try to find it
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
ScriptingObjectData data;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, data);
_objectsLocker.Unlock();
auto result = data.Ptr;
#else
ScriptingObject* result = nullptr;
_objectsLocker.Lock();
_objectsDictionary.TryGet(id, result);
_objectsLocker.Unlock();
#endif
// Check type
if (result && type && !result->Is(type))
{
result = nullptr;
}
return result;
}
ScriptingObject* Scripting::TryFindObject(MClass* type)
{
if (type == nullptr)
return nullptr;
ScopeLock lock(_objectsLocker);
for (auto i = _objectsDictionary.Begin(); i.IsNotEnd(); ++i)
{
const auto obj = i->Value;
if (obj->GetClass() == type)
return obj;
}
return nullptr;
}
ScriptingObject* Scripting::FindObject(const MObject* managedInstance)
{
if (managedInstance == nullptr)
return nullptr;
PROFILE_CPU();
// TODO: optimize it by reading the unmanagedPtr or _internalId from managed Object property
ScopeLock lock(_objectsLocker);
for (auto i = _objectsDictionary.Begin(); i.IsNotEnd(); ++i)
{
const auto obj = i->Value;
if (obj->GetManagedInstance() == managedInstance)
return obj;
}
return nullptr;
}
void Scripting::OnManagedInstanceDeleted(ScriptingObject* obj)
{
PROFILE_CPU();
ASSERT(obj);
// Validate if object still exists
_objectsLocker.Lock();
if (_objectsDictionary.ContainsValue(obj))
{
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[OnManagedInstanceDeleted] obj = 0x{0:x}, {1}", (uint64)obj, String(ScriptingObjectData(obj).TypeName));
#endif
obj->OnManagedInstanceDeleted();
}
else
{
//LOG(Warning, "Object finalization called for already removed object (address={0:x})", (uint64)obj);
}
_objectsLocker.Unlock();
}
bool Scripting::HasGameModulesLoaded()
{
return _hasGameModulesLoaded;
}
bool Scripting::IsEveryAssemblyLoaded()
{
auto& modules = BinaryModule::GetModules();
for (BinaryModule* module : modules)
{
if (!module->IsLoaded())
return false;
}
return true;
}
bool Scripting::IsTypeFromGameScripts(MClass* type)
{
const auto binaryModule = ManagedBinaryModule::GetModule(type ? type->GetAssembly() : nullptr);
return binaryModule && binaryModule != GetBinaryModuleCorlib() && binaryModule != GetBinaryModuleFlaxEngine();
}
void Scripting::RegisterObject(ScriptingObject* obj)
{
ScopeLock lock(_objectsLocker);
//ASSERT(!_objectsDictionary.ContainsValue(obj));
#if ENABLE_ASSERTION
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
ScriptingObjectData other;
if (_objectsDictionary.TryGet(obj->GetID(), other))
#else
ScriptingObject* other;
if (_objectsDictionary.TryGet(obj->GetID(), other))
#endif
{
// Something went wrong...
LOG(Error, "Objects registry already contains object with ID={0} (type '{3}')! Trying to register object {1} (type '{2}').", obj->GetID(), obj->ToString(), String(obj->GetClass()->GetFullName()), String(other->GetClass()->GetFullName()));
_objectsDictionary.Remove(obj->GetID());
}
#else
ASSERT(!_objectsDictionary.ContainsKey(obj->_id));
#endif
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[RegisterObject] obj = 0x{0:x}, {1}", (uint64)obj, String(ScriptingObjectData(obj).TypeName));
#endif
_objectsDictionary.Add(obj->GetID(), obj);
}
void Scripting::UnregisterObject(ScriptingObject* obj)
{
ScopeLock lock(_objectsLocker);
//ASSERT(!obj->_id.IsValid() || _objectsDictionary.ContainsValue(obj));
#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
LOG(Info, "[UnregisterObject] obj = 0x{0:x}, {1}", (uint64)obj, String(ScriptingObjectData(obj).TypeName));
#endif
_objectsDictionary.Remove(obj->GetID());
}
void Scripting::OnObjectIdChanged(ScriptingObject* obj, const Guid& oldId)
{
ScopeLock lock(_objectsLocker);
ASSERT(obj && oldId.IsValid());
ASSERT(obj->GetID() != oldId);
ASSERT(_objectsDictionary.ContainsKey(oldId));
//ASSERT(_objectsDictionary.ContainsValue(obj));
ASSERT(!_objectsDictionary.ContainsKey(obj->GetID()));
_objectsDictionary.Remove(oldId);
_objectsDictionary.Add(obj->GetID(), obj);
}
bool initFlaxEngine()
{
// Cache common types
if (StdTypesContainer::Instance()->Gather())
return true;
#if !COMPILE_WITHOUT_CSHARP
// Init C# class library
{
auto scriptingClass = Scripting::GetStaticClass();
ASSERT(scriptingClass);
const auto initMethod = scriptingClass->GetMethod("Init");
ASSERT(initMethod);
MObject* exception = nullptr;
initMethod->Invoke(nullptr, nullptr, &exception);
if (exception)
{
MException ex(exception);
ex.Log(LogType::Fatal, TEXT("FlaxEngine.Scripting.Init"));
return true;
}
}
#endif
// TODO: move it somewhere to game instance class or similar
MainRenderTask::Instance = New<MainRenderTask>();
return false;
}
void onEngineLoaded(MAssembly* assembly)
{
if (initFlaxEngine())
{
LOG(Fatal, "Failed to initialize Flax Engine runtime.");
}
// Set flag
_isEngineAssemblyLoaded = true;
}
void onEngineUnloading(MAssembly* assembly)
{
// Clear flag
_isEngineAssemblyLoaded = false;
// Clear cached methods
_method_Update = nullptr;
_method_LateUpdate = nullptr;
_method_FixedUpdate = nullptr;
_method_Exit = nullptr;
StdTypesContainer::Instance()->Clear();
}
void ScriptingService::Dispose()
{
Scripting::Release();
MCore::UnloadEngine();
}