I\'ve been experimenting with making a component based system similar to Unity\'s, but in C++. I\'m wondering how the GetComponent()
method that Unity implement
The Unity engine is linked with a forked mono runtime, on which unity scripts are executed.
In UnityEngine.Component
public class Component : Object
{
.
.
[TypeInferenceRule(TypeInferenceRules.TypeReferencedByFirstArgument)]
public Component GetComponent(Type type)
{
return this.gameObject.GetComponent(type);
}
[GeneratedByOldBindingsGenerator]
[MethodImpl(MethodImplOptions.InternalCall)]
internal extern void GetComponentFastPath(Type type, IntPtr oneFurtherThanResultValue);
[SecuritySafeCritical]
public unsafe T GetComponent()
{
CastHelper castHelper = default(CastHelper);
this.GetComponentFastPath(typeof(T), new IntPtr((void*)(&castHelper.onePointerFurtherThanT)));
return castHelper.t;
}
.
.
}
The C# code performs native calls, called Icalls to C++ methods that have been bound to the C# methods using the C# runtime library API. Bodyless (unimplemented) methods need either an extern
, abstract
or partial
specifier as a rule so all internal calls are marked as extern
. When the runtime sees a method with the [MethodImpl(MethodImplOptions.InternalCall)]
attribute it knows it needs to make an Icall, so it looks up the function it has been bound to and jumps to that address.
An Icall does not need to be static
in C# and automatically passes the this MonoObject
of the component to the C++ handler function. If they are static
then the this object is usually deliberately passed as a parameter using a C# shim method and making the shim method the static Icall. Using Icalls, types are not marshalled unless they are blittable types, meaning all other types are passed as MonoObject
, MonoString
etc.
Typically the C++ methods are functions or static methods but I think they can be non static methods as well, so long as they aren't virtual, because the address cannot be fixed by the runtime.
in UnityEngine.GameObject
public sealed class GameObject : Object
{
.
.
public GameObject(string name)
{
GameObject.Internal_CreateGameObject(this, name);
}
public GameObject()
{
GameObject.Internal_CreateGameObject(this, (string) null);
}
[WrapperlessIcall]
[TypeInferenceRule(TypeInferenceRules.TypeReferencedByFirstArgument)]
[MethodImpl(MethodImplOptions.InternalCall)]
public extern Component GetComponent(System.Type type);
[WrapperlessIcall]
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern void Internal_CreateGameObject([Writable] GameObject mono, string name);
.
.
}
The C# constructor for the GameObject
contains a call to a native method. The body of the constructor is run after initialisation of the C# object such that there is already a this pointer. Internal_CreateGameObject
is the static shim function that is actually called.
Someone's example implementation of their own C++ Internal_CreateGameObject
using mono:
bool GameObjectBinding::init()
{
MonoClass *gameObjectClass = Mono::get().getClass("GameObject");
gameObject_NativeID_Field = mono_class_get_field_from_name(gameObjectClass, "nativeID");
MonoClass *transformClass = Mono::get().getClass("Transform");
transform_NativeID_Field = mono_class_get_field_from_name(transformClass, "nativeID");
mono_add_internal_call("GameEngine_CS.GameObject::internal_createGameObject", GameObjectBinding::createGameObject);
mono_add_internal_call("GameEngine_CS.GameObject::internal_deleteGameObject", GameObjectBinding::deleteGameObject);
mono_add_internal_call("GameEngine_CS.GameObject::internal_getGameObject", GameObjectBinding::getGameObject);
mono_add_internal_call("GameEngine_CS.GameObject::internal_getTransform", GameObjectBinding::getTransform);
return true;
}
void GameObjectBinding::createGameObject(MonoObject * monoGameObject)
{
Object *newObject = LevelManager::get().getCurrentLevel()->createObject(0);
mono_field_set_value(monoGameObject, gameObject_NativeID_Field, (void*)newObject->getID());
}
mono_add_internal_call
has been used to bind this method to GameObjectBinding::createGameObject
, to which the this pointer is passed as a MonoObject
pointer. A native object is then created to represent the GameObject
, and mono_field_set_value
is then used to set the NativeID
field of the C# object to the ID of the new native object. This way the native object can be accessed from the MonoObject
which is the internal implementation of the C# object. The GameObject
is represented by 2 objects essentially.
public sealed class GameObject : Object
{
.
.
private UInt32 nativeID;
public UInt32 id { get { return nativeID; } }
.
.
}
This field is bound in the runtime using
mono_set_dirs( "/Library/Frameworks/Mono.framework/Home/lib", "/Library/Frameworks/Mono.framework/Home/etc" );
mono_config_parse( nullptr );
const char* managedbinarypath = "C:/Test.dll";
MonoDomain* domain = mono_jit_init(managedbinarypath)
MonoAssembly* assembly = mono_domain_assembly_open (domain, managedbinarypath);
MonoImage* image = mono_assembly_get_image (assembly);
MonoClass* gameobjectclass = mono_class_from_name(image, "ManagedLibrary", "GameObject");
gameObject_NativeID_Field = mono_class_get_field_from_name( gameobjectclass, "nativeID" );
GetComponent
passes typeof(T)
to GetComponentFastPath
(the native call) which passes the this pointer of the component as well. The native implementation of GetComponentFastPath
will receive this as a MonoObject*
and a MonoReflectionType*
for the type. The bound C++ method will then call mono_reflection_type_get_type()
on the MonoReflectionType*
to get the MonoType*
(here are the primitive types: https://github.com/samneirinck/cemono/blob/master/src/native/inc/mono/mono/metadata/blob.h), or for object types you can get the MonoClass*
from MonoType*
using mono_class_from_mono_type()
. It will then get the game object that is attached to the Component and search the components that the object has in some internal data structure.
Someone's example implementation of their own C++ GetComponent
using mono:
id ModuleScriptImporter::RegisterAPI()
{
//GAMEOBJECT
mono_add_internal_call("TheEngine.TheGameObject::CreateNewGameObject", (const void*)CreateGameObject);
mono_add_internal_call("TheEngine.TheGameObject::AddComponent", (const void*)AddComponent);
mono_add_internal_call("TheEngine.TheGameObject::GetComponent", (const void*)GetComponent);
}
MonoObject* ModuleScriptImporter::GetComponent(MonoObject * object, MonoReflectionType * type)
{
return current_script->GetComponent(object, type);
}
MonoObject* CSharpScript::GetComponent(MonoObject* object, MonoReflectionType* type)
{
if (!CheckMonoObject(object))
{
return nullptr;
}
if (currentGameObject == nullptr)
{
return nullptr;
}
MonoType* t = mono_reflection_type_get_type(type);
std::string name = mono_type_get_name(t);
const char* comp_name = "";
if (name == "CulverinEditor.Transform")
{
comp_name = "Transform";
}
MonoClass* classT = mono_class_from_name(App->importer->iScript->GetCulverinImage(), "CulverinEditor", comp_name);
if (classT)
{
MonoObject* new_object = mono_object_new(CSdomain, classT);
if (new_object)
{
return new_object;
}
}
return nullptr;
}
C# methods can be invoked from C++:
MonoMethodDesc* desc = mono_method_desc_new (const char *name, gboolean include_namespace);
MonoClass* class = mono_class_from_name (MonoImage *image, const char* name_space, const char *name);
MonoMethod* method = mono_method_desc_search_in_class (MonoMethodDesc *desc, MonoClass *klass);
MonoMethod* method = mono_method_desc_search_in_image (MonoMethodDesc *desc, MonoImage *image);
MonoObject* obj = mono_runtime_invoke (MonoMethod *method, void *obj, void **params,
MonoObject **exc);
See: https://gamedev.stackexchange.com/questions/115573/how-are-methods-like-awake-start-and-update-called-in-unity/183091#183091