Start of using CoreCLR and automatic scriptglue generation

This commit is contained in:
Simon Lübeß
2025-07-14 22:14:41 +02:00
parent 063cbfa415
commit edfcfd3b20
24 changed files with 1548 additions and 540 deletions
@@ -0,0 +1,185 @@
using NetHostBeef;
using System;
using System.Interop;
using GlitchyEngine.Core;
namespace GlitchyEngine.Scripting;
public struct ScriptFunctionPointers
{
public function void() OnCreate;
public function void(float) OnUpdate;
public function void() OnDestroy;
}
static class CoreClrHelper
{
static HostFxr.InitializeForDotnetCommandLineFn HostFxr_Init;
static HostFxr.GetRuntimeDelegateFn HostFxr_GetDelegate;
static HostFxr.CloseFn HostFxr_Close;
static CoreClr.LoadAssemblyAndGetFunctionPointerFn LoadAssemblyAndGetFunctionPointerFn;
static CoreClr.GetFunctionPointerFn GetFunctionPointerFn;
private function void LoadScriptAssemblyFunc(uint8* appData, int64 appLength, uint8* pdbData, int64 pdbLength);
//static function void(char8* path) LoadScriptAssembly;
static LoadScriptAssemblyFunc _loadScriptAssembly;
static function void() _unloadAssemblies;
// public static unsafe void GetScriptClasses(void** outBuffer, long* length)
private function void GetScriptClassesFunc(void** outBuffer, int64* length);
static GetScriptClassesFunc _getScriptClasses;
static function void() _freeScriptClassNames;
//public static unsafe ScriptFunctions CreateScriptInstance(UUID entityId, byte* scriptClassName)
private function void CreateScriptInstanceFunc(UUID entityId, char8* scriptClassName);
static CreateScriptInstanceFunc _createScriptInstance;
public static ScriptFunctionPointers _entityScriptFunctions;
public static void Init(StringView coreAssemblyPath)
{
LoadHostFxr();
InitAndStartRuntime(coreAssemblyPath);
PrepareFunction();
}
static void LoadHostFxr()
{
char_t[256] buffer = ?;
c_size bufferSize = buffer.Count;
int rc = NetHost.get_hostfxr_path(&buffer, &bufferSize, null);
Log.EngineLogger.AssertDebug(rc == 0, "Failed to get HostFxr path.");
// Load hostfxr and get desired exports
void* lib = NetHostHelper.LoadLibrary(&buffer);
HostFxr_Init = NetHostHelper.GetExport<HostFxr.InitializeForDotnetCommandLineFn>(lib, "hostfxr_initialize_for_dotnet_command_line");
HostFxr_GetDelegate = NetHostHelper.GetExport<HostFxr.GetRuntimeDelegateFn>(lib, "hostfxr_get_runtime_delegate");
HostFxr_Close = NetHostHelper.GetExport<HostFxr.CloseFn>(lib, "hostfxr_close");
Log.EngineLogger.AssertDebug(HostFxr_Init != null && HostFxr_GetDelegate != null && HostFxr_Close != null, "Retrieving at least one HostFxr function failed.");
}
static void InitAndStartRuntime(StringView coreAssembly)
{
char_t* ptr = NetHostHelper.GetScopedRawPtr!(coreAssembly);
char_t*[2] args = char_t*[](
ptr,
null
);
HostFxr.Handle cxt = null;
int rc = HostFxr_Init(1, &args, null, &cxt);
if (rc != 0 || cxt == null)
{
Log.EngineLogger.Error($"Failed to initialize HostFxr: {rc}");
HostFxr_Close(cxt);
}
Log.EngineLogger.Assert(rc == 0, "Failed to initialize HostFxr.");
rc = HostFxr_GetDelegate(cxt, .LoadAssemblyAndGetFunctionPointer, (void**)&LoadAssemblyAndGetFunctionPointerFn);
Log.EngineLogger.Assert(rc == 0, scope $"Get delegate failed: {rc}. (LoadAssemblyAndGetFunctionPointer)");
rc = HostFxr_GetDelegate(cxt, .GetFunctionPointer, (void**)&GetFunctionPointerFn);
Log.EngineLogger.Assert(rc == 0, scope $"Get delegate failed: {rc}. (GetFunctionPointer)");
HostFxr_Close(cxt);
}
static void PrepareFunction()
{
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "LoadScriptAssembly", out _loadScriptAssembly);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "UnloadAssemblies", out _unloadAssemblies);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "GetScriptClasses", out _getScriptClasses);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "FreeScriptClassNames", out _freeScriptClassNames);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "CreateScriptInstance", out _createScriptInstance);
InitEntityFunctions();
}
static void InitEntityFunctions()
{
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "InvokeEntityOnCreate", out _entityScriptFunctions.OnCreate);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "InvokeEntityOnUpdate", out _entityScriptFunctions.OnUpdate);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "InvokeEntityOnDestroy", out _entityScriptFunctions.OnDestroy);
}
public static void LoadAppAssembly(Span<uint8> appAssemblyData, Span<uint8> pdbData)
{
_loadScriptAssembly(appAssemblyData.Ptr, appAssemblyData.Length, pdbData.Ptr, pdbData.Length);
}
public static void UnloadAssemblies()
{
_unloadAssemblies();
}
/// Gets an array of script class infos. Use FreeScriptClassNames to release the buffer.
public static void GetScriptClasses(out void* outBuffer, out int64 entryCount)
{
outBuffer = null;
entryCount = 0;
_getScriptClasses(&outBuffer, &entryCount);
}
/// Releases the buffer created by GetScriptClasses
public static void FreeScriptClassNames()
{
_freeScriptClassNames();
}
public static void CreateScriptInstance(UUID entityId, StringView scriptName)
{
_createScriptInstance(entityId, scriptName.Ptr);
}
public static int GetFunctionPointer<T>(StringView typeName, StringView methodName, StringView delegateName, out T outDelegate) where T: operator explicit void*
{
void* funPtr = null;
int rc = GetFunctionPointerFn(NetHostHelper.GetScopedRawPtr!(typeName),
NetHostHelper.GetScopedRawPtr!(methodName), NetHostHelper.GetScopedRawPtr!(delegateName),
null, null, out funPtr);
outDelegate = (T)funPtr;
return rc;
}
public static int GetFunctionPointerDefaultDelegate(StringView typeName, StringView methodName, out CoreClr.DefaultEntryPoint outDelegate)
{
void* funPtr = null;
int rc = GetFunctionPointerFn(NetHostHelper.GetScopedRawPtr!(typeName),
NetHostHelper.GetScopedRawPtr!(methodName), null, null, null, out funPtr);
outDelegate = (CoreClr.DefaultEntryPoint)funPtr;
return rc;
}
public static int GetFunctionPointerUnmanagedCallersOnly<T>(StringView typeName, StringView methodName, out T outDelegate) where T: operator explicit void*
{
void* funPtr = null;
int rc = GetFunctionPointerFn(NetHostHelper.GetScopedRawPtr!(typeName),
NetHostHelper.GetScopedRawPtr!(methodName), CoreClr.UNMANAGEDCALLERSONLY_METHOD, null, null, out funPtr);
outDelegate = (T)funPtr;
return rc;
}
}
@@ -1,37 +1,28 @@
using System;
using Mono;
using GlitchyEngine.Core;
using System.Diagnostics;
namespace GlitchyEngine.Scripting;
using internal GlitchyEngine.Scripting;
class EntityEditorWrapper : ScriptClass
class EntityEditorWrapper : NewScriptClass
{
function void ShowEntityEditorFunc(MonoObject* scriptInstance, MonoException** exception);
function void ShowEntityEditorFunc(UUID entityId);
private ShowEntityEditorFunc _showEntityEditorFunc;
[AllowAppend]
public this(StringView classNamespace, StringView className, MonoImage* image) : base(classNamespace, className, image)
public this() : base(FullName, .Empty, .None, false)
{
_showEntityEditorFunc = (ShowEntityEditorFunc)GetMethodThunk("ShowEntityEditor", 1);
if (_showEntityEditorFunc == null)
{
Log.EngineLogger.Error("Entity editor has no show entity editor func.");
}
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "ShowEntityEditor", out _showEntityEditorFunc);
Debug.Assert(_showEntityEditorFunc != null);
}
public void ShowEntityEditor(ScriptInstance instance, UUID entityId)
public void ShowEntityEditor(NewScriptInstance instance, UUID entityId)
{
MonoException* exception = null;
_showEntityEditorFunc(instance.MonoInstance, &exception);
if (exception != null)
{
ScriptEngine.HandleMonoException(exception, entityId);
}
_showEntityEditorFunc(entityId);
}
}
@@ -0,0 +1,98 @@
using System;
using GlitchyEngine.Core;
using static GlitchyEngine.Scripting.ScriptEngine;
namespace GlitchyEngine.Scripting;
class NewScriptClass
{
public String FullName ~ delete _;
public StringView Name;
public Guid Guid;
public ScriptMethods Methods;
public bool RunInEditMode;
public this(StringView fullName, Guid guid, ScriptMethods methods, bool runInEditMode = false)
{
FullName = new String(fullName);
Guid = guid;
int lastDotIndex = FullName.LastIndexOf('.');
if (lastDotIndex == -1)
Name = FullName;
else
Name = FullName.Substring(lastDotIndex + 1);
Methods = methods;
RunInEditMode = runInEditMode;
}
}
using internal GlitchyEngine.Scripting;
class NewScriptInstance : RefCounter
{
private NewScriptClass _scriptClass;
private UUID _entityId;
private bool _isCreated = false;
public NewScriptClass ScriptClass => _scriptClass;
/// Gets whether or not the instance has ben initialized.
public bool IsInitialized {get; private set;};
/// Gets whether or not the Create-Method of this instance has been called before.
public bool IsCreated => _isCreated;
public UUID EntityId => _entityId;
public this(UUID entityId, NewScriptClass scriptClass)
{
Log.EngineLogger.AssertDebug(scriptClass != null);
_entityId = entityId;
_scriptClass = scriptClass;
}
public ~this()
{
Destroy();
}
public void Instantiate()
{
CoreClrHelper.CreateScriptInstance(_entityId, _scriptClass.FullName);
IsInitialized = true;
}
public void InvokeOnCreate()
{
if (_scriptClass.Methods.HasFlag(.OnCreate))
CoreClrHelper._entityScriptFunctions.OnCreate();
}
public void InvokeOnUpdate(float deltaTime)
{
if (_scriptClass.Methods.HasFlag(.OnUpdate))
CoreClrHelper._entityScriptFunctions.OnUpdate(deltaTime);
}
public void Destroy()
{
if (!IsInitialized)
return;
if (_scriptClass.Methods.HasFlag(.OnDestroy) && (ScriptEngine.ApplicationInfo.IsInPlayMode || ScriptClass.RunInEditMode))
{
CoreClrHelper._entityScriptFunctions.OnDestroy();
}
IsInitialized = false;
ScriptEngine.UnregisterScriptInstance(_entityId);
}
}
+6 -6
View File
@@ -81,8 +81,8 @@ class ScriptClass : SharpClass
// TODO: This is only relevant for the editor!
MonoCustomAttrInfo* attributes = Mono.mono_custom_attrs_from_class(_monoClass);
if (attributes != null && ScriptEngine.Classes.RunInEditModeAttribute != null)
_runInEditMode = Mono.mono_custom_attrs_has_attr(attributes, ScriptEngine.Classes.RunInEditModeAttribute._monoClass);
//if (attributes != null && ScriptEngine.Classes.RunInEditModeAttribute != null)
// _runInEditMode = Mono.mono_custom_attrs_has_attr(attributes, ScriptEngine.Classes.RunInEditModeAttribute._monoClass);
}
private MonoMethod* FindConstructor()
@@ -148,8 +148,8 @@ class ScriptClass : SharpClass
if (_onCollisionEnter2D != null)
{
MonoObject* monoObject = ScriptEngine.Classes.Collision2D.BoxValue(collision);
_onCollisionEnter2D(instance, monoObject, &exception);
//MonoObject* monoObject = ScriptEngine.Classes.Collision2D.BoxValue(collision);
//_onCollisionEnter2D(instance, monoObject, &exception);
}
}
@@ -159,8 +159,8 @@ class ScriptClass : SharpClass
if (_onCollisionEnter2D != null)
{
MonoObject* monoObject = ScriptEngine.Classes.Collision2D.BoxValue(collision);
_onCollisionLeave2D(instance, monoObject, &exception);
//MonoObject* monoObject = ScriptEngine.Classes.Collision2D.BoxValue(collision);
//_onCollisionLeave2D(instance, monoObject, &exception);
}
}
+107 -152
View File
@@ -15,31 +15,31 @@ using internal GlitchyEngine.Scripting;
class EngineClasses
{
// TODO: Not all of these are actually ScriptClasses (actually none of them are...)
private ScriptClass s_ComponentRoot;
private ScriptClass s_EntityRoot;
private ScriptClass s_EngineObject;
private NewScriptClass s_EngineObject;
private NewScriptClass s_ComponentRoot;
private NewScriptClass s_EntityRoot;
private EntityEditorWrapper s_EntityEditor;
private EntitySerializerWrapper s_EntitySerializer;
private ScriptClass s_SerializationContext;
//private NewScriptClass s_SerializationContext;
private ScriptClass s_Collision2D;
private NewScriptClass s_Collision2D;
private ScriptClass s_RunInEditModeAttribute;
public ScriptClass ComponentRoot => s_ComponentRoot;
public ScriptClass EntityRoot => s_EntityRoot;
public ScriptClass EngineObject => s_EngineObject;
private NewScriptClass s_RunInEditModeAttribute;
public NewScriptClass EngineObject => s_EngineObject;
public NewScriptClass ComponentRoot => s_ComponentRoot;
public NewScriptClass EntityRoot => s_EntityRoot;
public EntityEditorWrapper EntityEditor => s_EntityEditor;
public EntitySerializerWrapper EntitySerializer => s_EntitySerializer;
public ScriptClass Collision2D => s_Collision2D;
public NewScriptClass Collision2D => s_Collision2D;
public ScriptClass RunInEditModeAttribute => s_RunInEditModeAttribute;
public NewScriptClass RunInEditModeAttribute => s_RunInEditModeAttribute;
public ~this()
{
@@ -48,37 +48,37 @@ class EngineClasses
internal void ReleaseAndNullify()
{
ReleaseRefAndNullify!(s_EngineObject);
ReleaseRefAndNullify!(s_EntityRoot);
ReleaseRefAndNullify!(s_ComponentRoot);
DeleteAndNullify!(s_EngineObject);
DeleteAndNullify!(s_EntityRoot);
DeleteAndNullify!(s_ComponentRoot);
ReleaseRefAndNullify!(s_EntityEditor);
DeleteAndNullify!(s_EntityEditor);
ReleaseRefAndNullify!(s_EntitySerializer);
ReleaseRefAndNullify!(s_SerializationContext);
//ReleaseRefAndNullify!(s_SerializationContext);
ReleaseRefAndNullify!(s_Collision2D);
DeleteAndNullify!(s_Collision2D);
ReleaseRefAndNullify!(s_RunInEditModeAttribute);
DeleteAndNullify!(s_RunInEditModeAttribute);
}
internal void LoadClasses(MonoImage* image)
internal void LoadClasses()
{
ReleaseAndNullify();
s_EngineObject = new ScriptClass("GlitchyEngine.Core", "EngineObject", image);
s_EntityRoot = new ScriptClass("GlitchyEngine", "Entity", image);
s_ComponentRoot = new ScriptClass("GlitchyEngine.Core", "Component", image);
s_EngineObject = new NewScriptClass("GlitchyEngine.Core.EngineObject", .Empty, .None);
s_EntityRoot = new NewScriptClass("GlitchyEngine.Core.Entity", .Empty, .None);
s_ComponentRoot = new NewScriptClass("GlitchyEngine.Core.Component", .Empty, .None);
// Editor classes
s_EntityEditor = new EntityEditorWrapper("GlitchyEngine.Editor", "EntityEditor", image);
s_EntityEditor = new EntityEditorWrapper();
s_EntitySerializer = new EntitySerializerWrapper("GlitchyEngine.Serialization", "EntitySerializer", image);
//s_EntitySerializer = new EntitySerializerWrapper("GlitchyEngine.Serialization", "EntitySerializer");
s_Collision2D = new ScriptClass("GlitchyEngine.Physics", "Collision2D", image);
s_Collision2D = new NewScriptClass("GlitchyEngine.Physics.Collision2D", .Empty, .None);
// Attributes
s_RunInEditModeAttribute = new ScriptClass("GlitchyEngine.Editor", "RunInEditModeAttribute", image);
s_RunInEditModeAttribute = new NewScriptClass("GlitchyEngine.Editor.RunInEditModeAttribute", .Empty, .None);
}
}
@@ -99,9 +99,9 @@ static class ScriptEngine
public static EngineClasses Classes => _classes;
private static Dictionary<StringView, ScriptClass> _entityScripts = new .() ~ DeleteDictionaryAndReleaseValues!(_);
private static Dictionary<StringView, NewScriptClass> _entityScripts = new .() ~ DeleteDictionaryAndValues!(_);
internal static Dictionary<UUID, ScriptInstance> _entityScriptInstances = new .() ~ {
internal static Dictionary<UUID, NewScriptInstance> _entityScriptInstances = new .() ~ {
for (var entry in _)
{
entry.value?.ReleaseRef();
@@ -109,7 +109,7 @@ static class ScriptEngine
delete _;
}
public static Dictionary<StringView, ScriptClass> EntityClasses => _entityScripts;
public static Dictionary<StringView, NewScriptClass> EntityClasses => _entityScripts;
/// Gets or sets the current scene context for the runtime.
public static Scene Context
@@ -189,7 +189,8 @@ static class ScriptEngine
public static void Init()
{
InitMono();
InitRuntime();
//InitMono();
LoadScriptAssemblies();
}
@@ -202,7 +203,17 @@ static class ScriptEngine
ReloadAssemblies();
}
static void InitMono()
static void InitRuntime()
{
// TODO: We currently have the utility methods (e.g. assembly loading) in ScriptCore,
// the problem is, that CoreCLR currently can't unload the initial assembly.
// this means that we cannot easily reload changes to ScriptCore.
// Since this basic infrastructure shouldn't really ever change we could make a tiny
// library with only that enabling us to reload script core.
CoreClrHelper.Init("resources/scripts/ScriptCore.dll");
}
/*static void InitMono()
{
Mono.mono_set_assemblies_path("mono/lib/4.5");
@@ -226,7 +237,7 @@ static class ScriptEngine
}
Mono.mono_thread_set_main(Mono.mono_thread_current());
}
}*/
static bool _requestingReload = false;
@@ -278,26 +289,27 @@ static class ScriptEngine
ScriptGlue.Init();
CreateAppDomain("GlitchyEngineScriptRuntime");
(s_CoreAssembly, s_CoreAssemblyImage) = LoadAssembly("resources/scripts/ScriptCore.dll", _debuggingEnabled);
// TODO: Check if files exist
if (File.Exists(_appAssemblyPath))
(s_AppAssembly, s_AppAssemblyImage) = LoadAssembly(_appAssemblyPath, _debuggingEnabled);
else
{
s_AppAssembly = null;
s_AppAssemblyImage = null;
List<uint8> data = new:ScopedAlloc! List<uint8>(1024);
File.ReadAll(_appAssemblyPath, data);
List<uint8> pdbData = new:ScopedAlloc! List<uint8>(1024);
String pdbPath = scope .();
Path.ChangeExtension(_appAssemblyPath, ".pdb", pdbPath);
File.ReadAll(pdbPath, pdbData);
CoreClrHelper.LoadAppAssembly(data, pdbData);
GetEntitiesFromAssemblies();
//ScriptGlue.RegisterManagedComponents();
}
Classes.LoadClasses(s_CoreAssemblyImage);
ClearDictionaryAndReleaseValues!(_entityScripts);
GetEntitiesFromAssemblies();
ScriptGlue.RegisterManagedComponents();
InitAssemblyWatcher();
//InitAssemblyWatcher();
}
/// Starts the script runtime and sets the context scene.
@@ -338,14 +350,16 @@ static class ScriptEngine
/// Disposes of and replaces the old instance, if one exists.
public static bool InitializeInstance(Entity entity, ScriptComponent* script)
{
ScriptClass scriptClass = GetScriptClass(script.ScriptClassName);
Log.EngineLogger.Error($"{Compiler.CallerMemberName} not updated yet.");
NewScriptClass scriptClass = GetScriptClass(script.ScriptClassName);
if (scriptClass == null)
return false;
UUID entityId = entity.UUID;
script.Instance = new ScriptInstance(entityId, scriptClass);
script.Instance = new NewScriptInstance(entityId, scriptClass);
script.Instance..ReleaseRef();
if (_entityScriptInstances.TryGetValue(entityId, let currentInstance))
@@ -353,7 +367,7 @@ static class ScriptEngine
_entityScriptInstances[entityId] = script.Instance..AddRef();
script.Instance.Instantiate(entityId);
script.Instance.Instantiate();
return true;
}
@@ -381,109 +395,50 @@ static class ScriptEngine
_entityScriptInstances.Remove(entityId);
}
private static MonoAssembly* LoadCSharpAssembly(StringView assemblyPath, bool loadPDB = false)
public enum ScriptMethods : uint32
{
Debug.Profiler.ProfileFunction!();
List<uint8> data = new List<uint8>(1024);
File.ReadAll(assemblyPath, data);
// NOTE: We can't use this image for anything other than loading the assembly because this image doesn't have a reference to the assembly
MonoImageOpenStatus status = .ImageInvalid;
MonoImage* image = Mono.mono_image_open_from_data_full(data.Ptr, (.)data.Count, true, &status, false);
delete data;
if (status != .Ok)
{
char8* errorMessage = Mono.mono_image_strerror(status);
Log.EngineLogger.Error($"Failed to load C# Assembly: \"{StringView(errorMessage)}\"");
return null;
}
if (loadPDB)
{
String pdbPath = scope .();
Path.ChangeExtension(assemblyPath, ".pdb", pdbPath);
if (File.Exists(pdbPath))
{
Log.EngineLogger.Trace($"Loading PDB \"{pdbPath}\"...");
List<uint8> pdbData = new List<uint8>(1024);
let result = File.ReadAll(pdbPath, pdbData);
if (result case .Err(let error))
{
Log.EngineLogger.Error("Failed to load PDB file ({error}).");
}
Mono.mono_debug_open_image_from_memory(image, pdbData.Ptr, (int32)pdbData.Count);
delete pdbData;
}
else
{
Log.EngineLogger.Warning($"Debugging enabled but no PDB-File found \"{assemblyPath}\".");
}
}
MonoAssembly* assembly = Mono.mono_assembly_load_from_full(image, assemblyPath.ToScopeCStr!(), &status, 0);
Mono.mono_image_close(image);
return assembly;
None = 0,
OnCreate = 0x1,
OnUpdate = 0x2,
OnDestroy = 0x4,
}
static void CreateAppDomain(StringView name)
struct ScriptClassInfo
{
s_AppDomain = Mono.mono_domain_create_appdomain(name.ToScopeCStr!(), null);
Mono.mono_domain_set(s_AppDomain, true);
}
static (MonoAssembly* assembly, MonoImage* image) LoadAssembly(StringView filepath, bool loadPDB = false)
{
Debug.Profiler.ProfileFunction!();
MonoAssembly* assembly = LoadCSharpAssembly(filepath, loadPDB);
MonoImage* image = Mono.mono_assembly_get_image(assembly);
return (assembly, image);
public char8* Name;
public Guid Guid;
public ScriptMethods Methods;
public bool RunInEditMode;
}
private static void GetEntitiesFromAssemblies()
{
Debug.Profiler.ProfileFunction!();
if (s_AppAssemblyImage == null)
return;
// TODO: We should probably verify, that all required types actually exist.
Classes.LoadClasses();
MonoTableInfo* typeDefinitionsTable = Mono.mono_image_get_table_info(s_AppAssemblyImage, .MONO_TABLE_TYPEDEF);
int32 numTypes = Mono.mono_table_info_get_rows(typeDefinitionsTable);
CoreClrHelper.GetScriptClasses(let data, let entryCount);
Span<ScriptClassInfo> scriptClasses = .((.)data, entryCount);
List<NewScriptClass> newClasses = scope .();
for (int32 i = 0; i < numTypes; i++)
{
int32[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_SIZE] cols = .();
Mono.mono_metadata_decode_row(typeDefinitionsTable, i, (.)&cols, (.)SOME_RANDOM_ENUM.MONO_TYPEDEF_SIZE);
ClearDictionaryAndDeleteValues!(_entityScripts);
char8* nameSpace = Mono.mono_metadata_string_heap(s_AppAssemblyImage, (.)cols[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_NAMESPACE]);
char8* name = Mono.mono_metadata_string_heap(s_AppAssemblyImage, (.)cols[(.)SOME_RANDOM_ENUM.MONO_TYPEDEF_NAME]);
MonoClass* monoClass = Mono.mono_class_from_name(s_AppAssemblyImage, nameSpace, name);
// Check if it is an entity
if (monoClass != null && Mono.mono_class_is_subclass_of(monoClass, Classes.EntityRoot.[Friend]_monoClass, false))
{
ScriptClass entityScript = new ScriptClass(StringView(nameSpace), StringView(name), s_AppAssemblyImage);
_entityScripts.Add(entityScript.FullName, entityScript);
for (var entry in scriptClasses)
{
NewScriptClass scriptClass = new .(StringView(entry.Name), entry.Guid, entry.Methods, entry.RunInEditMode);
Log.EngineLogger.Info($"Added entity \"{entityScript.FullName}\"");
}
}
newClasses.Add(scriptClass);
}
for (var scriptClass in newClasses)
{
_entityScripts.Add(scriptClass.FullName, scriptClass);
}
CoreClrHelper.FreeScriptClassNames();
}
public static void ReloadAssemblies()
@@ -492,20 +447,20 @@ static class ScriptEngine
Log.EngineLogger.Info("Reloading script assemblies.");
ScriptInstanceSerializer contextSerializer = scope .();
contextSerializer.SerializeScriptInstances();
Mono.mono_domain_set(s_RootDomain, true);
Mono.mono_domain_unload(s_AppDomain);
// TODO: Unload script assemblies, remove class handles, fire unload events?
LoadScriptAssemblies();
{
Debug.Profiler.ProfileScope!("Initialize Instances");
// We need to create a new instance for every entity
Log.EngineLogger.Error("Recreating and copying instances (ReloadAssemblies) not updated yet.");
/*// We need to create a new instance for every entity
for (let (id, scriptInstance) in _entityScriptInstances)
{
if (Context.GetEntityByID(id) case .Ok(let entity))
@@ -517,7 +472,7 @@ static class ScriptEngine
{
Log.EngineLogger.AssertDebug(false, "Entities script was just serialized but the entity doesn't exist anymore.");
}
}
}*/
}
contextSerializer.DeserializeScriptInstances();
@@ -527,25 +482,25 @@ static class ScriptEngine
public static void Shutdown()
{
Mono.mono_domain_set(s_RootDomain, false);
/*Mono.mono_domain_set(s_RootDomain, false);
Mono.mono_domain_unload(s_AppDomain);
s_AppDomain = null;
Mono.mono_jit_cleanup(s_RootDomain);
s_RootDomain = null;
s_RootDomain = null;*/
}
/// Returns the script instance or null.
public static MonoObject* GetManagedInstance(UUID entityId)
{
if (_entityScriptInstances.TryGetValue(entityId, let scriptInstance))
return scriptInstance.MonoInstance;
//if (_entityScriptInstances.TryGetValue(entityId, let scriptInstance))
// return scriptInstance.MonoInstance;
return null;
}
public static ScriptClass GetScriptClass(StringView name)
public static NewScriptClass GetScriptClass(StringView name)
{
EntityClasses.TryGetValue(name, let scriptClass);
+311 -28
View File
@@ -14,7 +14,10 @@ using GlitchyEngine.Editor;
using GlitchyEngine.World.Components;
using GlitchyEngine.Content;
using GlitchyEngine.Renderer;
using System.Diagnostics;
using System.IO;
using static GlitchyEngine.Renderer.Text.FontRenderer;
using System.Linq;
namespace GlitchyEngine.Scripting;
@@ -39,22 +42,250 @@ class MessageOrigin
}
}
struct RegisterCallAttribute : Attribute
{
public String MethodName;
public this(String methodName)
{
MethodName = methodName;
}
}
struct TypeTranslationTemplate
{
public String StartCode;
public String EndCode;
public String PrettyCSharpParamType;
public this(String startCode, String endCode, String prettyCSharpParamType)
{
StartCode = startCode;
EndCode = endCode;
PrettyCSharpParamType = prettyCSharpParamType;
}
}
[AttributeUsage(.Struct)]
struct EngineFunctionsGeneratorAttribute : Attribute, IComptimeTypeApply
{
private static readonly Dictionary<String, String> BeefToCsharpTypeMap = new .()
{
("int32", "int"),
("char16*", "char*"),
("char8*", "byte*"),
("GlitchyEngine.Events.MouseButton", "GlitchyEngine.MouseButton")
};
// {0}... Out name,
// {1}... In name
private static readonly Dictionary<String, TypeTranslationTemplate> BeefTypeToCSharpWrapperTemplate = new .()
{
("char8*", .("byte* {0} = (byte*)Marshal.StringToCoTaskMemUTF8({1});", "Marshal.FreeCoTaskMem((IntPtr){0});", "string")),
("char16*", .("fixed (char* {0} = {1})\n{{", "}}", "string")),
};
private static readonly Dictionary<String, TypeTranslationTemplate> CSharpTypeToReturnValueTemplate = new .()
{
("void", .("", "", "void")),
};
private static String GetCSharpInterfaceType(String beefType)
{
if (BeefToCsharpTypeMap.TryGetValueAlt(beefType, let csharpType))
{
return csharpType;
}
return beefType;
}
private static TypeTranslationTemplate GetCSharpWrapperTemplate(String beefType)
{
if (BeefTypeToCSharpWrapperTemplate.TryGetValueAlt(beefType, let template))
{
return template;
}
return .("", "", GetCSharpInterfaceType(beefType));
}
private static TypeTranslationTemplate GetCSharpReturnValueTemplate(String beefType)
{
if (CSharpTypeToReturnValueTemplate.TryGetValueAlt(beefType, let template))
{
return template;
}
return .("var returnValue = ", "return returnValue;", GetCSharpInterfaceType(beefType));
}
private static void GenerateCSharpFunctionPointer(MethodInfo method, String outString)
{
String parameters = new String();
for (int i < method.ParamCount)
{
if (i != 0)
parameters.Append(", ");
String typeHolder = scope String();
method.GetParamType(i).ToString(typeHolder);
StringView csharpType = GetCSharpInterfaceType(typeHolder);
parameters.AppendF($"{csharpType}");
}
String csharpFunctionPointer = scope $" public delegate* unmanaged[Cdecl]<{parameters}{(parameters.IsEmpty ? "" : ", ")}{method.ReturnType}> {method.Name};\n";
outString.Append(csharpFunctionPointer);
}
private static void GenerateCSharpWrapperMethod(MethodInfo method, String outString)
{
String wrapperParameters = new .();
String callArguments = new .();
String translation = new .();
String cleanup = new .();
for (int i < method.ParamCount)
{
String beefParameterType = scope String();
method.GetParamType(i).ToString(beefParameterType);
TypeTranslationTemplate template = GetCSharpWrapperTemplate(beefParameterType);
StringView paramName = method.GetParamName(i);
// Generate the parameter for the wrapper head.
{
if (i != 0)
wrapperParameters.Append(", ");
wrapperParameters.AppendF($"{template.PrettyCSharpParamType} {paramName}");
}
{
if (i != 0)
callArguments.Append(", ");
String translatedParamName = new String(paramName);
if (!template.StartCode.IsEmpty)
{
translatedParamName.Append("Converted");
callArguments.AppendF($"{translatedParamName}");
translation.AppendF(template.StartCode, translatedParamName, paramName);
translation.Append('\n');
}
else
{
callArguments.AppendF($"{paramName}");
}
if (!template.EndCode.IsEmpty)
{
cleanup.AppendF(template.EndCode, translatedParamName, paramName);
cleanup.Append('\n');
}
}
}
String returnTypeName = new .();
method.ReturnType.ToString(returnTypeName);
String csharpReturnType = GetCSharpInterfaceType(returnTypeName);
TypeTranslationTemplate template = GetCSharpReturnValueTemplate(csharpReturnType);
outString.AppendF($"""
internal static unsafe {template.PrettyCSharpParamType} NEW_{method.Name}({wrapperParameters})
{{
{translation}
{template.StartCode}_engineFunctions.{method.Name}({callArguments});
{cleanup}
{template.EndCode}
}}
""");
}
[Comptime]
public void ApplyToType(Type self)
{
String csharpEngineFunctionsStruct = new String("""
using System;
using System.Runtime.InteropServices;
using GlitchyEngine;
using GlitchyEngine.Core;
namespace GlitchyEngine;
internal unsafe partial struct EngineFunctions
{
""");
String csharpScriptGlue = new String("""
internal static partial class ScriptGlue
{
""");
for (MethodInfo methodInfo in typeof(ScriptGlue).GetMethods(.Static | .NonPublic))
{
if (methodInfo.GetCustomAttribute<RegisterCallAttribute>() case .Ok(let attribute))
{
String parameters = new String();
for (int i < methodInfo.ParamCount)
{
if (i != 0)
parameters.Append(", ");
parameters.AppendF($"{methodInfo.GetParamType(i)}");
}
String line = scope $"public function {methodInfo.ReturnType}({parameters}) {methodInfo.Name};\n";
Compiler.EmitTypeBody(self, line);
GenerateCSharpFunctionPointer(methodInfo, csharpEngineFunctionsStruct);
GenerateCSharpWrapperMethod(methodInfo, csharpScriptGlue);
}
}
csharpEngineFunctionsStruct.Append('}');
csharpScriptGlue.Append('}');
csharpEngineFunctionsStruct.Append(csharpScriptGlue);
if (File.WriteAllText("../ScriptCore/ScriptGlue.gen.cs", csharpEngineFunctionsStruct) case .Err)
{
Runtime.FatalError("Failed to write file");
}
}
}
[EngineFunctionsGenerator]
struct EngineFunctions
{
}
static class ScriptGlue
{
private static Dictionary<MonoType*, function void(Entity entityId)> s_AddComponentMethods = new .() ~ delete _;
private static Dictionary<MonoType*, function bool(Entity entityId)> s_HasComponentMethods = new .() ~ delete _;
private static Dictionary<MonoType*, function void(Entity entityId)> s_RemoveComponentMethods = new .() ~ delete _;
struct RegisterCallAttribute : Attribute
{
public String MethodName;
public this(String methodName)
{
MethodName = methodName;
}
}
/* Adding this attribute to a method will log method entry and returned Result<T> errors */
[AttributeUsage(.Method)]
struct RegisterMethodAttribute : Attribute, IOnMethodInit
@@ -62,27 +293,47 @@ static class ScriptGlue
[Comptime]
public void OnMethodInit(MethodInfo method, Self* prev)
{
String functionContent = new .();
int i = 0;
for (var methodInfo in typeof(ScriptGlue).GetMethods(.Static))
{
if (methodInfo.GetCustomAttribute<RegisterCallAttribute>() case .Ok(let attribute))
{
String functionType = scope $"{methodInfo.ReturnType}({methodInfo.GetParamsDecl(.. scope .())})";
String line = scope $"RegisterCall<function {functionType}>(\"{attribute.MethodName}\", => {methodInfo.Name});\n";
Compiler.EmitMethodEntry(method, line);
functionContent.AppendF($"functions.{methodInfo.Name} = => {methodInfo.Name};\n");
//functionContent.AppendF($"functions[{i}] = (void*)( => {methodInfo.Name});\n");
i++;
}
}
functionContent.Insert(0, scope $"EngineFunctions functions = .();\n");
functionContent.Append("_setEngineFunctions(&functions);");
Compiler.EmitMethodEntry(method, functionContent);
}
}
public static Event<delegate void()> OnRegisterNativeCalls ~ _.Dispose();
/*private function void SetEngineFunctions(EngineFunctions* engineFunctions);
private static SetEngineFunctions _setEngineFunctions;*/
private function void SetEngineFunctions(EngineFunctions* engineFunctions);
private static SetEngineFunctions _setEngineFunctions;
public static void Init()
{
if (_setEngineFunctions == null)
{
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "SetEngineFunctions", out _setEngineFunctions);
}
RegisterCalls();
RegisterMathFunctions();
//RegisterMathFunctions();
}
public static void RegisterManagedComponents()
@@ -103,11 +354,18 @@ static class ScriptGlue
RegisterComponent<MeshRendererComponent>("GlitchyEngine.Graphics.MeshRenderer");
}
[RegisterMethod]
private static void RegisterCalls()
{
FillEngineFunctions();
OnRegisterNativeCalls.Invoke();
}
[RegisterMethod]
private static void FillEngineFunctions()
{
}
private static void RegisterComponent<T>(StringView cSharpClassName = "") where T : struct, new
{
@@ -248,7 +506,7 @@ static class ScriptGlue
#region Log
[RegisterCall("ScriptGlue::Log_LogMessage")]
/*[RegisterCall("ScriptGlue::Log_LogMessage")]
static void Log_LogMessage(int32 logLevel, MonoString* message, MonoString* fileName, int lineNumber)
{
char8* utfMessage = Mono.mono_string_to_utf8(message);
@@ -275,6 +533,30 @@ static class ScriptGlue
}
Mono.mono_free(utfMessage);
}*/
[RegisterCall("ScriptGlue::Log_LogMessage")]
[CallingConvention(.Cdecl)]
static void Log_LogMessage(int32 logLevel, char16* messagePtr, char16* fileNamePtr, int lineNumber)
{
String escapedMessage = new:ScopedAlloc! String(messagePtr);
// Why exactly do we have to replace these? Can we get away without copying the string above?
escapedMessage.Replace("{", "{{");
escapedMessage.Replace("}", "}}");
if (fileNamePtr != null)
{
String fileName = new:ScopedAlloc! String(fileNamePtr);
MessageOrigin messageOrigin = scope MessageOrigin(fileName, lineNumber);
Log.ClientLogger.Log((LogLevel)logLevel, escapedMessage, messageOrigin);
}
else
{
Log.ClientLogger.Log((LogLevel)logLevel, escapedMessage);
}
}
[RegisterCall("ScriptGlue::Log_LogException")]
@@ -457,8 +739,8 @@ static class ScriptGlue
scriptComponent = entity.GetComponent<ScriptComponent>();
}
if (scriptComponent.Instance != null)
ScriptEngine.Context.DestroyScriptDeferred(scriptComponent.Instance, false);
//if (scriptComponent.Instance != null)
// ScriptEngine.Context.DestroyScriptDeferred(scriptComponent.Instance, false);
scriptComponent.Instance = null;
@@ -470,7 +752,8 @@ static class ScriptGlue
// TODO: this returns false, if no script with ScriptClassName exists, we have to handle this case correctly I think.
ScriptEngine.InitializeInstance(entity, scriptComponent);
return scriptComponent.Instance.MonoInstance;
//return scriptComponent.Instance.MonoInstance;
return null;
}
[RegisterCall("ScriptGlue::Entity_RemoveScript")]
@@ -478,7 +761,7 @@ static class ScriptGlue
{
ScriptComponent* scriptComponent = GetComponentSafe<ScriptComponent>(entityId);
ScriptEngine.Context.DestroyScriptDeferred(scriptComponent.Instance, true);
//ScriptEngine.Context.DestroyScriptDeferred(scriptComponent.Instance, true);
}
[RegisterCall("ScriptGlue::Entity_GetName")]
@@ -653,7 +936,7 @@ static class ScriptGlue
}
[Packed]
struct AxisAngle
public struct AxisAngle
{
public float3 Axis;
public float Angle;
@@ -1014,8 +1297,8 @@ static class ScriptGlue
innerRadius = circleRenderer.InnerRadius;
}
[RegisterCall("ScriptGlue::CircleRenderer_GetInnerRadius")]
static void CircleRenderer_GetInnerRadius(UUID entityId, float innerRadius)
[RegisterCall("ScriptGlue::CircleRenderer_SetInnerRadius")]
static void CircleRenderer_SetInnerRadius(UUID entityId, float innerRadius)
{
CircleRendererComponent* circleRenderer = GetComponentSafe<CircleRendererComponent>(entityId);
circleRenderer.InnerRadius = innerRadius;
@@ -1085,8 +1368,8 @@ static class ScriptGlue
#region TextRenderer
[RegisterCall("ScriptGlue::TextRenderer_SetIsRichText")]
static bool TextRenderer_SetIsRichText(UUID entityId)
[RegisterCall("ScriptGlue::TextRenderer_GetIsRichText")]
static bool TextRenderer_GetIsRichText(UUID entityId)
{
return GetComponentSafe<TextRendererComponent>(entityId).IsRichText;
}
@@ -1419,7 +1702,7 @@ static class ScriptGlue
}
[RegisterCall("ScriptGlue::Asset_SetIdentifier")]
static void Asset_GetIdentifier(UUID assetId, MonoString* text)
static void Asset_SetIdentifier(UUID assetId, MonoString* text)
{
ThrowNotImplementedException("Asset.GetIdentifier is not implemented.");
@@ -23,12 +23,12 @@ public class ScriptInstanceSerializer
public void Clear()
{
ClearDictionaryAndDeleteValues!(_serializedData);
ScriptEngine.Classes.EntitySerializer.DestroySerializationContext(this);
//ScriptEngine.Classes.EntitySerializer.DestroySerializationContext(this);
}
public void Init()
{
ScriptEngine.Classes.EntitySerializer.CreateSerializationContext(this);
//ScriptEngine.Classes.EntitySerializer.CreateSerializationContext(this);
}
/// Serializes all script instances that are currently managed by the ScriptEngine.
@@ -36,7 +36,11 @@ public class ScriptInstanceSerializer
{
Debug.Profiler.ProfileFunction!();
Init();
Log.EngineLogger.Error("SerializeScriptInstances not implemented.");
return;
/*Init();
for (let (id, scriptInstance) in ScriptEngine._entityScriptInstances)
{
@@ -46,7 +50,7 @@ public class ScriptInstanceSerializer
for (let (name, scriptClass) in ScriptEngine.EntityClasses)
{
SerializeStaticScriptClassFields(scriptClass);
}
}*/
}
/// Serializes the given script instance.
@@ -67,8 +71,12 @@ public class ScriptInstanceSerializer
public void DeserializeScriptInstances()
{
Debug.Profiler.ProfileFunction!();
Log.EngineLogger.Error("SerializeScriptInstances not implemented.");
Init();
return;
/*Init();
for (let (id, script) in ScriptEngine._entityScriptInstances)
{
@@ -78,7 +86,7 @@ public class ScriptInstanceSerializer
for (let (name, scriptClass) in ScriptEngine.EntityClasses)
{
DeserializeStaticScriptClassFields(scriptClass);
}
}*/
}
/// Deserializes the data into the given script instance, if there is data available.
@@ -674,7 +674,7 @@ namespace GlitchyEngine.World
{
private String _scriptClassName = null;
private ScriptInstance _instance = null;
private NewScriptInstance _instance = null;
public StringView ScriptClassName
{
@@ -688,7 +688,7 @@ namespace GlitchyEngine.World
}
}
public ScriptInstance Instance
public NewScriptInstance Instance
{
[Inline]
get => _instance;
+12 -11
View File
@@ -394,9 +394,10 @@ namespace GlitchyEngine.World
collision.OtherEntity = colliderEntityB.UUID;
collision.Rigidbody = rigidbodyEntityA.UUID;
collision.OtherRigidbody = rigidbodyEntityB.UUID;
scriptOfColliderA?.Instance?.InvokeOnCollisionEnter2D(collision);
scriptOfRigidbodyA?.Instance?.InvokeOnCollisionEnter2D(collision);
// TODO:
//scriptOfColliderA?.Instance?.InvokeOnCollisionEnter2D(collision);
//scriptOfRigidbodyA?.Instance?.InvokeOnCollisionEnter2D(collision);
}
bool fireEventB = colliderEntityB.TryGetComponent<ScriptComponent>(let scriptOfColliderB);
@@ -410,8 +411,8 @@ namespace GlitchyEngine.World
collision.Rigidbody = rigidbodyEntityB.UUID;
collision.OtherRigidbody = rigidbodyEntityA.UUID;
scriptOfColliderB?.Instance?.InvokeOnCollisionEnter2D(collision);
scriptOfRigidbodyB?.Instance?.InvokeOnCollisionEnter2D(collision);
//scriptOfColliderB?.Instance?.InvokeOnCollisionEnter2D(collision);
//scriptOfRigidbodyB?.Instance?.InvokeOnCollisionEnter2D(collision);
}
};
_contactListener.endContactCallback = (contact, userData) => {
@@ -446,8 +447,8 @@ namespace GlitchyEngine.World
collision.Rigidbody = rigidbodyEntityA.UUID;
collision.OtherRigidbody = rigidbodyEntityB.UUID;
scriptOfColliderA?.Instance?.InvokeOnCollisionLeave2D(collision);
scriptOfRigidbodyA?.Instance?.InvokeOnCollisionLeave2D(collision);
//scriptOfColliderA?.Instance?.InvokeOnCollisionLeave2D(collision);
//scriptOfRigidbodyA?.Instance?.InvokeOnCollisionLeave2D(collision);
}
bool fireEventB = colliderEntityB.TryGetComponent<ScriptComponent>(let scriptOfColliderB);
@@ -461,8 +462,8 @@ namespace GlitchyEngine.World
collision.Rigidbody = rigidbodyEntityB.UUID;
collision.OtherRigidbody = rigidbodyEntityA.UUID;
scriptOfColliderB?.Instance?.InvokeOnCollisionLeave2D(collision);
scriptOfRigidbodyB?.Instance?.InvokeOnCollisionLeave2D(collision);
//scriptOfColliderB?.Instance?.InvokeOnCollisionLeave2D(collision);
//scriptOfRigidbodyB?.Instance?.InvokeOnCollisionLeave2D(collision);
}
};
@@ -992,13 +993,13 @@ namespace GlitchyEngine.World
if (sourceScript.Instance != null)
{
scriptSerializer.SerializeScriptInstance(sourceScript.Instance);
//scriptSerializer.SerializeScriptInstance(sourceScript.Instance);
// Initializes the created instance
// TODO: this returns false, if no script with ScriptClassName exists, we have to handle this case correctly I think.
ScriptEngine.InitializeInstance(copy, targetScript);
newScripts.Add((original.UUID, targetScript.Instance));
//newScripts.Add((original.UUID, targetScript.Instance));
}
}