Unity3D 架构
Unity3D 是一个广泛使用的游戏引擎,支持多种平台的游戏开发。它的架构主要由两部分组成:
-
非托管代码(Unmanaged Code):
- 这部分主要是用 C++ 编写的,负责引擎的底层功能,如图形渲染、物理计算、音频处理等。
- 非托管代码直接与操作系统和硬件交互,通常具有更高的性能,但开发和调试相对复杂。
-
托管代码(Managed Code):
- Unity 使用 C# 作为主要的脚本语言,C# 代码在编译时会被转换为 CIL(Common Intermediate Language),这是一种中间语言。
- CIL 代码在运行时由 Mono Runtime 或 .NET Runtime 进行解释或编译为机器代码。
- 托管代码的内存管理由垃圾回收(Garbage Collection)机制处理,简化了开发者的工作。
Mono Runtime 的角色
Mono Runtime 是 Unity3D 中连接托管代码和非托管代码的关键技术。它的主要功能包括:
- CIL 执行:Mono Runtime 负责执行 C# 编写的托管代码,将 CIL 转换为机器代码。
- 内存管理:Mono 提供了垃圾回收机制,自动管理托管代码的内存分配和释放。
- 与非托管代码的交互:Mono 允许托管代码与非托管代码(C++)进行交互,开发者可以通过 P/Invoke(平台调用)机制调用 C++ 函数。
托管代码与非托管代码的区别
-
托管代码(Managed Code):
- 由 .NET 运行时(如 Mono)管理。
- 提供自动内存管理(垃圾回收)。
- 运行时安全性更高,减少了内存泄漏和访问违规的风险。
- 例如:C# 代码。
-
非托管代码(Unmanaged Code):
- 直接与操作系统和硬件交互。
- 需要手动管理内存,开发者负责分配和释放内存。
- 性能通常更高,但更容易出现内存泄漏和其他错误。
- 例如:C++ 代码。
总结
Unity3D 的架构通过 Mono Runtime 将托管代码(C#)与非托管代码(C++)连接起来,使得开发者能够利用 C# 的易用性和安全性,同时也能访问底层的高性能功能。这样的设计使得 Unity3D 成为一个强大且灵活的游戏开发平台,适合各种类型的游戏开发。
Mono Runtime 的角色
Mono Runtime 将托管代码(C#)与非托管代码(C++)连接起来,主要是指在 Unity 等应用程序中,如何实现这两种代码之间的交互和通信。以下是这一过程的详细解释:
1. 托管代码与非托管代码的定义
- 托管代码(Managed Code):由 .NET 运行时(如 Mono)管理的代码,具有自动内存管理(垃圾回收)和运行时安全性。C# 是最常用的托管语言。
- 非托管代码(Unmanaged Code):直接与操作系统和硬件交互的代码,通常需要手动管理内存。C++ 是最常用的非托管语言。
2. 连接的必要性
在 Unity 中,许多性能关键的功能(如图形渲染、物理计算等)是用 C++ 编写的,而游戏逻辑和脚本通常是用 C# 编写的。为了充分利用这两种语言的优势,必须实现它们之间的连接。
3. Mono Runtime 的角色
Mono Runtime 作为 .NET 的实现,负责执行托管代码,并提供与非托管代码交互的机制。它的主要功能包括:
- 执行 C# 代码:Mono 将 C# 代码编译为 CIL(Common Intermediate Language),并在运行时将其转换为机器代码执行。
- 内存管理:Mono 负责托管代码的内存管理,包括垃圾回收。
- 调用非托管代码:Mono 提供了多种机制,使得托管代码能够调用非托管代码。
4. 调用非托管代码的机制
在 Unity 中,托管代码与非托管代码之间的连接通常通过以下几种方式实现:
-
P/Invoke(平台调用):
- 这是 .NET 提供的一种机制,允许托管代码调用非托管代码中的函数。开发者可以使用
DllImport
特性来声明外部函数,并在 C# 中调用它们。 - 示例:
[DllImport("MyNativeLibrary")] public static extern int MyNativeFunction(int param);
- 这是 .NET 提供的一种机制,允许托管代码调用非托管代码中的函数。开发者可以使用
-
C++/CLI:
- C++/CLI 是一种可以同时使用托管和非托管代码的语言。它允许开发者在 C++ 中编写托管代码,并与 C# 进行交互。
-
Unity 的插件系统:
- Unity 允许开发者创建 C++ 插件,这些插件可以被 Unity 项目调用。通过这种方式,开发者可以将性能关键的代码封装在 C++ 中,并通过托管代码进行调用。
5. 数据传递
在托管代码和非托管代码之间传递数据时,开发者需要注意数据类型的匹配和转换。例如,C# 中的字符串和 C++ 中的字符串表示方式不同,开发者需要进行适当的转换。
总结
Mono Runtime 在 Unity 中的作用是作为桥梁,将托管代码(C#)与非托管代码(C++)连接起来。通过 P/Invoke 和其他机制,开发者可以在 C# 中调用 C++ 函数,从而充分利用两种语言的优势,构建高性能的应用程序。这样的设计使得 Unity 成为一个灵活且强大的游戏开发平台。
P/Invoke
P/Invoke(Platform Invocation Services)是 .NET 提供的一种机制,允许托管代码(如 C#)调用非托管代码(如 C/C++ 编写的 DLL)。这使得开发者能够利用现有的非托管库或系统 API,同时保持托管代码的安全性和易用性。
P/Invoke 的基本概念
- 非托管代码:通常是用 C/C++ 编写的动态链接库(DLL),可以直接与操作系统交互。
- 托管代码:由 .NET 运行时(如 Mono 或 .NET Core)管理的代码,具有自动内存管理和类型安全。
使用 P/Invoke 的步骤
使用 P/Invoke 调用非托管代码通常涉及以下几个步骤:
- 定义外部函数:在 C# 中使用
DllImport
特性来声明要调用的非托管函数。 - 调用函数:在托管代码中调用声明的函数。
- 数据类型匹配:确保托管代码和非托管代码之间的数据类型能够正确匹配。
示例
以下是一个简单的示例,演示如何使用 P/Invoke 调用一个非托管的 C 函数。
1. 创建非托管代码(C)
首先,创建一个简单的 C 函数并编译为 DLL:
// MyNativeLibrary.c
#include <stdio.h>
__declspec(dllexport) int Add(int a, int b) {
return a + b;
}
编译这个代码为 MyNativeLibrary.dll
。
2. 在 C# 中使用 P/Invoke
接下来,在 C# 中使用 P/Invoke 调用这个函数:
using System;
using System.Runtime.InteropServices;
class Program
{
// 声明外部函数
[DllImport("MyNativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int Add(int a, int b);
static void Main()
{
int result = Add(3, 4);
Console.WriteLine("Result: " + result); // 输出: Result: 7
}
}
关键点
- DllImport 特性:用于指定要调用的 DLL 名称和其他信息(如调用约定)。
- CallingConvention:指定调用约定,常用的有
CallingConvention.Cdecl
和CallingConvention.StdCall
。确保与非托管代码中的调用约定一致。 - 数据类型匹配:确保 C# 中的数据类型与 C/C++ 中的数据类型相匹配。例如,C# 的
int
对应 C/C++ 的int
。
注意事项
- 内存管理:在使用 P/Invoke 时,开发者需要注意内存管理,特别是在传递指针或引用时。
- 错误处理:非托管代码可能会抛出异常或返回错误代码,开发者需要适当处理这些情况。
- 性能:P/Invoke 调用会有一定的性能开销,因此在性能敏感的场景中应谨慎使用。
总结
P/Invoke 是一种强大的机制,使得托管代码能够调用非托管代码,允许开发者利用现有的 C/C++ 库和系统 API。通过正确的声明和数据类型匹配,开发者可以在 C# 中轻松地与非托管代码进行交互。
非托管代码具备的条件
非托管代码(Unmanaged Code)是指不受 .NET 运行时(如 CLR 或 Mono)管理的代码。它通常是用 C 或 C++ 等语言编写的,直接与操作系统和硬件交互。以下是非托管代码具备的一些主要条件和特性:
1. 内存管理
- 手动内存管理:非托管代码需要开发者手动分配和释放内存。开发者使用
malloc
、free
(C)或new
、delete
(C++)等操作来管理内存。 - 没有垃圾回收:与托管代码不同,非托管代码不具备自动垃圾回收机制,开发者需要确保在不再需要内存时释放它,以避免内存泄漏。
2. 直接与操作系统交互
- 系统调用:非托管代码可以直接调用操作系统的 API,进行文件操作、网络通信、线程管理等。
- 硬件访问:可以直接访问硬件资源,如内存地址、设备驱动等。
3. 编译方式
- 编译为机器代码:非托管代码通常被编译为特定平台的机器代码,而不是中间语言(如 CIL)。这使得它可以直接在操作系统上运行。
- 平台依赖性:非托管代码通常与特定的操作系统和硬件架构紧密相关,可能无法在不同的平台上直接运行。
4. 类型安全
- 缺乏类型安全:非托管代码不受 .NET 运行时的类型安全检查,开发者需要自行确保类型的正确性。这可能导致运行时错误和安全漏洞。
5. 异常处理
- 自定义异常处理:非托管代码的异常处理机制与托管代码不同,通常使用返回值或特定的错误代码来指示错误,而不是抛出异常。
6. 调用约定
- 调用约定:非托管代码使用特定的调用约定(如
cdecl
、stdcall
),这决定了函数参数的传递方式和栈的清理方式。开发者在调用非托管函数时需要确保使用正确的调用约定。
7. 语言特性
- 使用低级语言:非托管代码通常使用 C、C++、汇编语言等低级语言编写,这些语言提供了对硬件和系统资源的直接控制。
总结
非托管代码的主要特征包括手动内存管理、直接与操作系统交互、编译为机器代码、缺乏类型安全和异常处理机制等。这些特性使得非托管代码在性能和灵活性方面具有优势,但也增加了开发的复杂性和潜在的错误风险。
P/Invoke运行原理
P/Invoke(Platform Invocation Services)是 .NET 提供的一种机制,允许托管代码(如 C#)调用非托管代码(如 C/C++ 编写的 DLL)。P/Invoke 的运行原理涉及多个步骤和机制,以下是其基本工作原理的详细说明:
1. 声明外部函数
在 C# 中,使用 DllImport
特性来声明要调用的非托管函数。这个声明告诉 CLR 该函数位于哪个 DLL 中,以及如何调用它。
[DllImport("MyNativeLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int Add(int a, int b);
2. 编译和加载 DLL
当托管代码运行时,CLR 会根据 DllImport
声明查找并加载指定的 DLL。这个过程包括:
- 查找 DLL:CLR 会在系统的 PATH 环境变量中查找指定的 DLL,或者在应用程序的目录中查找。
- 加载 DLL:一旦找到 DLL,操作系统会将其加载到进程的地址空间中。
3. 参数转换
在调用非托管函数之前,CLR 会处理参数的转换。这包括:
- 数据类型映射:将 C# 中的数据类型转换为相应的非托管数据类型。例如,C# 的
int
通常映射到 C/C++ 的int
。 - 结构体和数组:如果参数是结构体或数组,CLR 会负责将这些数据从托管内存复制到非托管内存。
4. 调用约定
CLR 会根据 DllImport
声明中的 CallingConvention
属性确定如何调用非托管函数。常见的调用约定包括:
- Cdecl:调用者负责清理堆栈。
- StdCall:被调用者负责清理堆栈,通常用于 Windows API。
5. 执行非托管代码
一旦参数准备好,CLR 会通过调用操作系统的 API 来执行非托管函数。此时,控制权转移到非托管代码中。
6. 返回值处理
非托管函数执行完毕后,会返回结果。CLR 会负责将返回值从非托管代码转换回托管代码。例如,将 C/C++ 的 int
转换为 C# 的 int
。
7. 清理和释放资源
在函数调用完成后,CLR 会处理任何必要的清理工作,包括:
- 释放非托管内存:如果非托管函数分配了内存,开发者需要确保在适当的时机释放这些内存。
- 恢复托管环境:控制权返回到托管代码,CLR 继续执行后续的托管代码。
8. 异常处理
如果非托管代码抛出异常,CLR 需要处理这些异常。非托管代码的异常处理机制与托管代码不同,因此开发者需要在调用非托管代码时小心处理可能的错误。
总结
P/Invoke 的运行原理涉及多个步骤,从声明外部函数、加载 DLL、参数转换、调用非托管代码,到处理返回值和清理资源。通过这些机制,P/Invoke 使得托管代码能够安全、有效地与非托管代码进行交互。尽管 P/Invoke 提供了强大的功能,但开发者在使用时需要注意内存管理和数据类型匹配等问题,以确保代码的稳定性和安全性。
调用约定(Calling Conventions)
定义了函数参数的传递方式、返回值的处理方式以及谁负责清理堆栈。以下是您提到的两种常见调用约定的详细说明:
1. Cdecl(C Declaration)
- 定义:Cdecl 是 C 语言的默认调用约定,允许函数接受可变数量的参数。
- 堆栈清理:调用者负责清理堆栈。这意味着在函数调用完成后,调用该函数的代码需要手动调整堆栈指针,以移除传递给函数的参数。
- 优点:
- 支持可变参数函数(如
printf
)。 - 允许在同一程序中使用不同的调用约定。
- 支持可变参数函数(如
- 缺点:
- 由于调用者负责清理堆栈,可能导致堆栈不匹配的错误,尤其是在参数数量不一致时。
2. StdCall(Standard Call)
- 定义:StdCall 是 Windows API 的标准调用约定,通常用于 Windows 系统中的 API 函数。
- 堆栈清理:被调用者负责清理堆栈。这意味着在函数执行完毕后,函数本身会调整堆栈指针,移除传递给它的参数。
- 优点:
- 由于被调用者负责清理堆栈,减少了调用者的负担,降低了堆栈不匹配的风险。
- 在调用约定中,参数的顺序和大小是固定的,便于优化。
- 缺点:
- 不支持可变参数函数。
其他常见调用约定
除了 Cdecl 和 StdCall,还有其他一些调用约定,例如:
- FastCall:通过寄存器传递前几个参数,以提高性能。通常用于性能敏感的代码。
- ThisCall:用于 C++ 成员函数调用,
this
指针通常通过寄存器传递。
总结
选择合适的调用约定对于确保函数调用的正确性和性能至关重要。在使用 P/Invoke 调用非托管代码时,开发者需要明确指定调用约定,以确保托管代码和非托管代码之间的参数传递和堆栈清理能够正确匹配。
标签:调用,C#,代码,托管,C++,unity,内存 From: https://blog.csdn.net/qq_33060405/article/details/143670319