翻译 from peter1745的《mono-guide》
强烈建议阅读Mono官方文档
文章目录
字段和属性(Fields & Properties)
C# 中,field和property是不同的。字段和属性在底层实现上是不同的概念。字段是直接存储数据的地方,而属性是通过方法(get、set)来进行封装的。因此,获取字段的指针和获取属性的指针涉及到不同的内部处理逻辑。
字段:是类中存储数据的成员,它直接包含了数据。字段通常用于表示对象的状态或属性。
属性: 属性是一种通过方法来访问、设置或计算的成员。属性通常用于控制对类的字段的访问,允许在读取或写入字段值时执行特定的逻辑。
在这一部分,我们将介绍如何获取对C#字段和属性的引用,并与它们进行交互。之所以同时涵盖属性和字段,是因为Mono允许我们与属性进行交互,就好像它们是常规字段一样(几乎是),尽管属性本质上只是围绕字段和两个方法(get set)的语法糖。
如果您想了解有关 C# 属性的更多信息,请查看 Microsoft Docs 中的这篇文章。
现在,与 Mono 中的大多数内容一样,有多种方法可以获取对 C# 字段或属性的引用,您将使用哪一种取决于您是否想要迭代所有字段和属性,或者是否想要获取任何特定字段或属性。同样重要的是要注意,与方法一样,我们不是从类的实例中获取字段或属性,而是从类本身获取字段或属性,然后我们只需使用类实例访问字段或属性。
看一下代码,我们对C# 类做了一些修改
(1) 获取字段和成员属性的引用
using System;
public class CSharpTesting
{
public float MyPublicFloatVar = 5.0f;
private string m_Name = "Hello";
public string Name
{
get => m_Name;
set
{
m_Name = value;
MyPublicFloatVar += 5.0f;
}
}
public void PrintFloatVar()
{
Console.WriteLine("MyPublicFloatVar = {0:F}", MyPublicFloatVar);
}
private void IncrementFloatVar(float value)
{
MyPublicFloatVar += value;
}
}
在C++这一边:
// 补充一个工具函数:实例化一个类对象(分配内存并调用默认构造)
MonoObject* InstantiateClass(MonoClass* monoClass)
{
MonoObject* instance = mono_object_new(s_Data->AppDomain, monoClass);
mono_runtime_object_init(instance);
return instance;
}
MonoObject* testingInstance = InstantiateClass("", "CSharpTesting");
MonoClass* testingClass = mono_object_get_class(testingInstance);
// Get a reference to the public field called "MyPublicFloatVar"
MonoClassField* floatField = mono_class_get_field_from_name(testingClass, "MyPublicFloatVar");
// Get a reference to the private field called "m_Name"
MonoClassField* nameField = mono_class_get_field_from_name(testingClass, "m_Name");
// Get a reference to the public property called "Name"
MonoProperty* nameProperty = mono_class_get_property_from_name(testingClass, "Name");
// ....Do something
代码解释:
现在我们有了一些非常基本的代码,可以让我们获得对我们想要的所有字段和属性的引用。
在向您展示如何与它们交互之前,我想花点时间解释一下:Mono 不关心关于我们想要访问的类、方法、字段或属性的可访问性。您想设置私有字段的值吗?Mono 表示可以。想要在一个loop中间把一个私有成员变量设置为 null 并导致整个应用程序崩溃?Mono 表示也可以。
这当然意味着我们有责任尊重这种性质(可以访问私有属性)。出于可调试性原因能够访问这些字段或属性可能非常有用。
但我想说的是,您的脚本引擎永远不应该设置这些私有字段或属性的值,除非该代码的编写者明确允许。
例如,您可以添加一个 C# 属性,用户可以将其添加到其私有字段中,告诉脚本引擎:它可以设置该字段的值,在 Hazel 中,有一个名为 ShowInEditor
的属性,它表示允许引擎设置该字段的值。 Unity 有 SerializeField
可以做同样的事情。
好吧,现在让我们看看如何获得我们刚获取的字段和属性的可访问性。
(2) 检查成员的访问权限
enum class Accessibility : uint8_t
{
None = 0,
Private = (1 << 0),
Internal = (1 << 1),
Protected = (1 << 2),
Public = (1 << 3)
};
// 获取特定字段的访问级别
uint8_t GetFieldAccessibility(MonoClassField* field)
{
uint8_t accessibility = (uint8_t)Accessibility::None;
uint32_t accessFlag = mono_field_get_flags(field) & MONO_FIELD_ATTR_FIELD_ACCESS_MASK;
switch (accessFlag)
{
case MONO_FIELD_ATTR_PRIVATE:
{
accessibility = (uint8_t)Accessibility::Private;
break;
}
case MONO_FIELD_ATTR_FAM_AND_ASSEM:
{
accessibility |= (uint8_t)Accessibility::Protected;
accessibility |= (uint8_t)Accessibility::Internal;
break;
}
case MONO_FIELD_ATTR_ASSEMBLY:
{
accessibility = (uint8_t)Accessibility::Internal;
break;
}
case MONO_FIELD_ATTR_FAMILY:
{
accessibility = (uint8_t)Accessibility::Protected;
break;
}
case MONO_FIELD_ATTR_FAM_OR_ASSEM:
{
accessibility |= (uint8_t)Accessibility::Private;
accessibility |= (uint8_t)Accessibility::Protected;
break;
}
case MONO_FIELD_ATTR_PUBLIC:
{
accessibility = (uint8_t)Accessibility::Public;
break;
}
}
return accessibility;
}
// 获取成员属性的访问级别
uint8_t GetPropertyAccessbility(MonoProperty* property)
{
uint8_t accessibility = (uint8_t)Accessibility::None;
// 获取该成员属性的getter方法的指针(引用)
MonoMethod* propertyGetter = mono_property_get_get_method(property);
if (propertyGetter != nullptr)
{
// 从getter方法中提取访问级别
uint32_t accessFlag = mono_method_get_flags(propertyGetter, nullptr) & MONO_METHOD_ATTR_ACCESS_MASK;
switch (accessFlag)
{
case MONO_FIELD_ATTR_PRIVATE:
{
accessibility = (uint8_t)Accessbility::Private;
break;
}
case MONO_FIELD_ATTR_FAM_AND_ASSEM:
{
accessibility |= (uint8_t)Accessbility::Protected;
accessibility |= (uint8_t)Accessbility::Internal;
break;
}
case MONO_FIELD_ATTR_ASSEMBLY:
{
accessibility = (uint8_t)Accessbility::Internal;
break;
}
case MONO_FIELD_ATTR_FAMILY:
{
accessibility = (uint8_t)Accessbility::Protected;
break;
}
case MONO_FIELD_ATTR_FAM_OR_ASSEM:
{
accessibility |= (uint8_t)Accessibility::Private;
accessibility |= (uint8_t)Accessibility::Protected;
break;
}
case MONO_FIELD_ATTR_PUBLIC:
{
accessibility = (uint8_t)Accessbility::Public;
break;
}
}
}
// 获取setter函数的指针
MonoMethod* propertySetter = mono_property_get_set_method(property);
if (propertySetter != nullptr)
{
// Extract the access flags from the setters flags
uint32_t accessFlag = mono_method_get_flags(propertySetter, nullptr) & MONO_METHOD_ATTR_ACCESS_MASK;
if (accessFlag != MONO_FIELD_ATTR_PUBLIC)
accessibility = (uint8_t)Accessibility::Private;
}
else
{
accessibility = (uint8_t)Accessibility::Private;
}
return accessibility;
}
// ...
MonoObject* testingInstance = InstantiateClass("", "CSharpTesting");
MonoClass* testingClass = mono_object_get_class(testingInstance);
// 获取类中字段的指针(公共字段)
MonoClassField* floatField = mono_class_get_field_from_name(testingClass, "MyPublicFloatVar");
uint8_t floatFieldAccessibility = GetFieldAccessibility(floatField);
if (floatFieldAccessibility & (uint8_t)Accessibility::Public)
{
// 我们可以安全的写入数据到该字段
}
// 获取类中字段的指针(私有字段)
MonoClassField* nameField = mono_class_get_field_from_name(testingClass, "m_Name");
uint8_t nameFieldAccessibility = GetFieldAccessibility(nameField);
if (nameFieldAccessibility & (uint8_t)Accessibility::Private)
{
// 不应该做写入操作
}
// 获取属性"Name"的指针(公共属性)
MonoProperty* nameProperty = mono_class_get_property_from_name(testingClass, "Name");
uint8_t namePropertyAccessibility = GetPropertyAccessibility(nameProperty);
if (namePropertyAccessibility & (uint8_t)Accessibility::Public)
{
// 可以写入值到该字段
}
// .....Do something
您可能会注意到,我们将可访问性存储在位字段(bit field)中,原因是 C# 允许您将类字段和属性标记为protected internal
或private protected
。在这种情况下,我们将 Accessibility::Protected
和 Accessibility::Internal
存储在同一个变量中会更有效。现在,您完全有可能仅仅只是关心字段/属性是否是公共的,在这种情况下,您可以设计一个函数如IsFieldPublic
/ IsPropertyPublic
,只返回 bool 即可
GetFieldAccessibility
能看懂代码情忽略这(1)(2)的冗长的说明
GetFieldAccessibility
函数的工作原理非常简单,我们首先通过获取传递的字段上设置的所有标志(其中存储了不仅是可访问性数据的更多信息),然后我们通过使用按位 AND 运算符对标志和 MONO_FIELD_ATTR_FIELD_ACCESS_MASK
掩码来从中提取可访问性数据。
这将为我们提供一个代表以下可能访问类型之一的值:
- MONO_FIELD_ATTR_PRIVATE
- MONO_FIELD_ATTR_FAM_AND_ASSEM
- MONO_FIELD_ATTR_ASSEMBLY
- MONO_FIELD_ATTR_FAMILY
- MONO_FIELD_ATTR_FAM_OR_ASSEM
- MONO_FIELD_ATTR_PUBLIC
如果你想知道每个值代表什么,可以查看代码。因此,我们在访问标志上执行一个 switch
语句,并检查这些值中的每一个,然后根据我们遇到的每个 case
语句,简单地将正确的 Accessibility
值分配给 accessibility
变量。
GetPropertyAccessibility
从代码中你可能已经注意到,获取属性的可访问性并不像获取字段那么简单。这是因为属性本质上代表两个方法:一个 getter
和一个 setter
。这意味着我们不能直接查询属性的可访问性,而是必须查询方法的可访问性。这变得更加复杂,因为属性不需要同时具有 getter 和 setter,只需要其中一个。如果你创建一个自动属性,例如 public string MyProp => m_MyValue;,C# 只会生成一个 getter,而不是 setter。
因此,我们首先通过调用 mono_property_get_get_method
并传递属性来获取对 getter 方法的引用。然后,我们通过调用 mono_method_get_flags
并传递 getter 方法以及 nullptr
从该 getter(如果存在)获取可访问性标志。最后一个参数仅表示方法实现标志。如果你对这些标志感兴趣,可以查看这篇文章。我们传递 nullptr
是因为我们现在对这些标志不感兴趣。
就像我们在 GetFieldAccessibility
中所做的那样,我们执行按位 AND
运算以获取可访问性标志,对其进行 switch 操作,并将结果存储在 accessibility
变量中。
在完成这一步之后,我们对 setter
做了类似的操作(如果存在的话)。不同之处在于对于 setter,我们仅检查它是否不是公共的,在这种情况下,我们将可访问性变量设置为 Accessibility::Private
,因为如果 setter 是私有的,我们无法写入属性。如果没有 setter,我们还将可访问性设置为私有。
获取 getter 和 setter 之间的另一个区别是对于 setter,我们显然必须调用 mono_property_get_set_method
而不是 mono_property_get_get_method
。
(3) 获取/设置值
这可能是一个非常复杂的主题,特别是当你开始处理实际的数据封送(marshalling data)时,不过现在我们不会过多涉及这方面。
marshalling :不同编程环境或者数据表示之间进行数据传递的过程,涉及格式的调整和转换。
- 具体而言,在不同的语言或环境之间传递数据时,可能会涉及到数据类型、内存布局、字节序等方面的差异。因此,进行数据驱动是确保数据在传递过程中能够正确映射和转换的关键步骤。
- 在本文语境中,C++ 和 C# 交互时,我们需要考虑到这两种语言之间的数据格式差异,以确保数据能够被正确地传递和理解。
我们将从获取和设置MyPublicFloatVar
的值开始,然后再转到对Name
属性进行相同操作。
bool CheckMonoError(MonoError& error)
{
bool hasError = !mono_error_ok(&error);
if (hasError)
{
unsigned short errorCode = mono_error_get_error_code(&error);
const char* errorMessage = mono_error_get_message(&error);
printf("Mono Error!\n");
printf("\tError Code: %hu\n", errorCode);
printf("\tError Message: %s\n", errorMessage);
mono_error_cleanup(&error);
}
return hasError;
}
std::string MonoStringToUTF8(MonoString* monoString)
{
if (monoString == nullptr || mono_string_length(monoString) == 0)
return "";
MonoError error;
char* utf8 = mono_string_to_utf8_checked(monoString, &error);
if (CheckMonoError(error))
return "";
std::string result(utf8);
mono_free(utf8);
return result;
}
获取/设置字段
// 分配对象并获取其指针和类指针
MonoObject* testingInstance = InstantiateClass("", "CSharpTesting");
MonoClass* testingClass = mono_object_get_class(testingInstance);
MonoClassField* floatField = mono_class_get_field_from_name(testingClass, "MyPublicFloatVar");
float value;
mono_field_get_value(testingInstance, floatField, &value);
value += 10.0f;
mono_field_set_value(testingInstance, floatField, &value);
获取/设置属性
MonoProperty* nameProperty = mono_class_get_property_from_name(testingClass, "Name");
// 获取Name属性的引用,通过mono_property_get_value调用getter方法
MonoString* nameValue = (MonoString*)mono_property_get_value(nameProperty, testingInstance, nullptr, nullptr);
std::string nameStr = MonoStringToUTF8(nameValue);
// 通过 mono_property_set_value调用setter方法
nameStr += ", World!";
nameValue = mono_string_new(s_AppDomain, nameStr.c_str());
mono_property_set_value(nameProperty, testingInstance, (void**)&nameValue, nullptr);
代码解释:
字段
有很多内容需要理解。我们先从mono_field_get_value
函数开始。该函数有三个参数,第一个是我们要从中获取值的类实例,第二个是要获取值的字段,第三个是指向在 C++ 端保存该值的变量的指针。
在这里,你需要理解的其中一件重要的事情是获取值类型(如float、int或struct)与获取引用类型(如class)之间的区别。
- 如果你要获取值类型,只要该值没有被封箱(
boxed
),就可以简单地声明一个相应的C++类型的变量,并传递该变量的内存地址。但是,C++ 类型的大小必须与 C# 类型的大小相匹配,如果使用结构体,布局也必须匹配。 - 如果你要获取引用类型,你必须声明一个
MonoObject
指针,因为引用类型总是分配在堆上。
好的,有了这个基础,我们可以递增该值,并通过调用mono_field_set_value
将其重新赋值给字段。这时,我们传入类实例、字段以及变量的内存地址,假设这是一个值类型。
如果你想确认 C# 字段是否被正确更新,你可以简单地再次检索该值并进行检查。当然,你可能希望建立一个灵活的系统,以更好地处理这些转换,并增加类型安全性。因为 Mono 只要值的大小与 C# 字段的大小匹配,就会简单地接受该值,并且很可能不会告诉你有任何问题。
属性
这与处理字段时相比有一些不同。我将不会详细解释CheckMonoError
或MonoStringToUTF8
,我会留到后面再讨论。简而言之,MonoStringToUTF8函数接收一个MonoString
指针,该指针简单地保存了指向C#托管字符串的指针,然后将其复制到非托管内存中并返回一个std::string
。
而CheckMonoError
函数简单地从给定的MonoError结构中提取错误代码和消息,然后将其记录到控制台。
首先,我们调用mono_property_get_value
,这将为我们调用属性的getter
方法。前两个参数分别是属性本身和类实例。我们传递nullptr
表示getter
方法可能期望的任何参数,最后一个参数是一个MonoException*
指针,我们可以从方法中获取,以防它抛出异常。在我看来,第三个参数实际上并不合理,因为我不知道属性的getter方法可以接受任何参数,所以我总是传递nullptr。
正如你所见,我们获取了一个字符串,因此我们只需得到一个指向MonoString
结构的指针,它在托管内存中保存了指向C#字符串的指针。需要注意的是,mono_property_get_value
返回一个MonoObject
指针,在字符串的情况下应该简单地转换为MonoString
指针。
例如,在获取值类型(如float
)的情况下,它将返回封箱在MonoObject
内的值,这意味着我们必须对其进行取消封箱(unboxing
)。我强烈建议你在这里了解装箱和取消封箱。
但由于在这种情况下我们获取的是一个字符串,我们只需进行类型转换,然后将其转换为std::string
。正如你所看到的,我们使用了mono_string_to_utf8_checked
,我想提到,如果有一个“checked”版本的Mono函数可用,你应该始终使用它,而不是未检查的版本。此函数还返回一个指向缓冲区的指针,我们负责在使用完毕后释放它。请记住这一点。
如果你想了解如何处理取消封箱值类型(如float),请查看我下面编写的代码。
好的,现在我们已经得到了我们的字符串并对其进行了修改,是时候将其重新分配给C#了。我相信你已经意识到我们在这里进行了很多复制,但不用太担心,因为你很可能不会在每一帧都从C++获取和设置C#值。
为了调用属性的setter函数,我们调用mono_property_set_value
,就像在getter中一样,我们传递属性和类实例。然后,我们必须传递值本身。最后一个参数还是一个指向MonoObject*
的指针,将包含setter抛出的任何异常。我再次传递nullptr。
关于mono_property_set_value
的重要一点是,你不能像使用mono_field_set
那样直接传递值类型的内存地址。如果你这样做,这个函数会崩溃。
相反,你需要创建一个void*
数组并将地址存储在该数组中。我不确定为什么会这样,但确实是这样。所以你需要类似这样的东西:void* data[] = { &myValueTypeData };
,然后像这样传递该数组:mono_property_set_value(prop, instance, data, nullptr);
。
对于MonoObject*
或MonoString*
,你只需传递内存地址并将其转换为void**
。这里,我建议始终使用void*数组方法,因为它始终有效。
你可能注意到在上面的示例中我们调用了mono_string_new
,并传递了AppDomain
以及C字符串,以构造一个新的MonoString
。我们这样做是因为我们不能,也不应该尝试修改从属性获取的MonoString
。我不会过多地详细讨论字符串,因为我稍后会深入介绍它们。
处理值类型的属性
// 通过属性名获取 MonoProperty* 对象
MonoProperty* floatProperty = mono_class_get_property_from_name(testingClass, "MyFloatProperty");
// 通过调用 getter 方法获取 Name 属性的值
MonoObject* floatValueObj = mono_property_get_value(floatProperty, testingInstance, nullptr, nullptr);
float floatValue = *(float*)mono_object_unbox(floatValueObj);
// 修改值并将其赋回属性,通过调用 setter 方法
floatValue += 10.0f;
void* data[] = {
&floatValue };
mono_property_set_value(floatProperty, testingInstance, data, nullptr);
好了!现在我们已经完成了字段和属性的基础知识。是的,我知道我称这为基础有些讽刺,考虑到这篇文章的长度,但重要的是你真正理解它是如何工作的。
在处理字段和属性方面,这些只是最基本的内容,其他的还请查阅官方文档。
在下一节中,我们将介绍内部调用(Internal Calls),这将允许 C# 代码调用 C++ 函数。
文章评论