)
Mono 代码编写指南dotnet/runtime 仓库 Mono 运行时 C 代码贡献规范【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime本指南面向在 src/mono/mono、src/native/public/mono 区域贡献代码的开发者系统性地梳理了 Mono 运行时的 C 代码风格、命名约定、宏定义、类型选择、C11 新特性使用规范、平台抽象层、目录依赖关系、错误处理、内部调用Internal Calls、GC 挂起安全Suspend Safety、GC 内存安全、断言、公共 API 稳定性等核心准则。读完本文你将能够按照 dotnet/runtime 仓库的 Mono 代码规范提交符合要求的 C 代码并理解 Mono 与 CoreCLR 在架构理念上的关键差异。适用范围本指南适用于以下代码区域src/mono/monoMono 运行时本体eglib、utils、sgen、metadata、mini 等src/native/public/monoMono 公共 API 头文件不适用于src/native 中的共享原生代码src/mono 中 System.Private.CoreLib 等 Mono 特有的 C# 代码代码风格Mono 使用 C 语言编写遵循 Mono 官方的编码规范其中最重要的三点是使用 Tab 缩进不使用空格函数名与左括号之间留一个空格如foo (bar)花括号与if、for、while等关键字放在同一行KR 风格这种风格与 CoreCLR 的代码风格明显不同在跨仓库贡献代码时需要特别注意转换。命名规范保留前缀Mono 为符号保留了以下前缀前缀用途mono_大多数函数monovm_虚拟机相关符号m_部分内联访问函数与宏monoeg_用于重映射 eglib 函数源码中带g_前缀所有非static符号必须使用上述前缀之一。eglib 的 remap 机制可以在 eglib-remap.h 中查看它把g_前缀的 GLib 兼容函数映射为mono_eg_前缀。类型命名单个 C 文件内的类型可以使用任意名称头文件中的类型应使用Mono或有时mono_前缀公共 API 的符号和类型必须加前缀。Mono 惯用的类型定义模式是typedef struct _MonoWhatever { ... } MonoWhatever;不透明类型Opaque types则可以在客户端头文件中只声明typedef struct _MonoWhatever MonoWhatever;而在实现头文件中定义struct _MonoWhatever {...}。有时为了打破#include循环也会加入一些类型的向前声明。宏定义Mono 源自 autotools 风格的项目因此在宏命名上有严格的约定HOST_XYZ表示运行时实际执行所在的机器而非编译运行时的那台机器TARGET_XYZ表示JIT 和 AOT 编译器将要面向的目标机器。在 AOT 编译场景下host 与 target 可能不同例如 host 是 Windows而 target 是 Browser WebAssembly。宏通常使用MONO_前缀公共 API 头文件中的宏必须使用该前缀。类型选择优先使用标准 C 的定长类型int32_t、intptr_t等而不是 eglib 的类型gint32、gsize等。唯一的例外是gboolean优于 C 的bool。实际运行时有三种布尔类型需要区分类型用途gboolean运行时内部使用MonoBoolean内部调用internal calls中与 C#bool互操作的类型mono_bool公共 C API 使用一般新代码不应使用它除非是新增公共 API 函数C11 及更新特性Mono 目前2023 年使用 C11 编写。静态断言应大量使用static_assert并包含assert.h。如果不能包含assert.h例如引入了冲突的符号或宏则改用g_static_assert。不要直接使用_Static_assert原因是 C23 已弃用_Static_assert可查看g_static_assert的定义方式作为参考。线程、锁与 call_once由于 Mono 的线程模型和协作式 GC直接使用 C 的线程与锁原语是不被允许的。应优先使用具备 GC 感知能力的锁MonoCoopMutex、MonoCoopCond具有 GC 感知的加锁/等待操作mono_mutex_t、mono_cond_t等仅用于 GC 转换代价过高、且可以保证锁永远不会被 GC 发起者获取、或不会被处于 GC 协作模式与抢占模式的混合线程获取的场景。从源码实现来看mono-coop-mutex.hMonoCoopMutex的mono_coop_mutex_lock会先尝试mono_os_mutex_trylock快速路径只有在锁被争用时才进入MONO_ENTER_GC_SAFE区域后再阻塞加锁这正是GC 感知的关键所在。而在运行时之外的本地库 P/Invoke 中使用标准 C 线程和call_once是可以的使用标准 C 互斥锁和条件变量也是可以的前提是这些锁不与运行时内部共享。线程局部存储文档暂未给出明确指引FIXME: no guidance yet。原子操作C 标准原子操作不能保证无锁lock-free因此应使用 Mono 的mono_atomic_系列函数其中一些在特定平台上可能基于标准 C 原子实现。Mono 不希望引入锁因为锁不是 GC 感知的可能导致协作式 GC 死锁。在 atomic.h 中可以看到完整的原子操作族例如mono_atomic_cas_i32、mono_atomic_cas_i64、mono_atomic_cas_ptrCAS、mono_atomic_add_i32/i64、mono_atomic_inc_i32/i64、mono_atomic_load_*等。在 Mono 中使用_Atomic被视为代码异味code smell在运行时之外的本地库 P/Invoke 中只要这些原子操作不被运行时内部同时访问使用标准 C 原子操作是可以的。泛型操作_Generic目前也暂无指引FIXME: no guidance for_Genericyet。工具函数与平台抽象Mono 与 CoreCLR 在平台抽象理念上存在显著差异Mono在缺乏 POSIX 抽象的平台上如 Windows补充 POSIX 风格的抽象CoreCLR PAL在 POSIX 之上补充 Windows API 抽象。这是一个相反的哲学方向理解这一点有助于在阅读两个运行时代码时不产生混淆。如果已有 eglib 工具函数可用应优先使用也可以新增。其他平台相关代码位于 src/mono/utils注意该目录名在仓库中实际为src/mono/mono/utils。目录依赖关系对于 src/mono/mono 下的代码目录依赖关系是严格分层的目录允许依赖eglib不依赖src/mono中其他代码utilseglib src/native/external 的第三方代码sgeneglibutilsmetadata以上全部注意该目录中部分 Boehm-GC 文件不应依赖sgenmini以上全部mini/interp以上全部components以上全部中标记了MONO_COMPONENT_API的函数参见 docs/design/mono/components.mdmetadata与utils的核心区别在于utils 代码不应假设自己属于正在运行的 .NET 运行时的一部分——任何加载类型、创建对象等操作都不属于 utils而属于 metadata。mini目录包含执行引擎execution engines。如果执行引擎需要向metadata提供某些功能通常的做法是安装回调callback由metadata在合适时机调用。例如mini知道如何展开异常、执行栈回溯stack walksmetadata决定何时展开异常或执行栈回溯。为协调两者metadata暴露安装钩子的 APImini提供具体实现。错误处理MonoError新代码应优先使用MonoError函数族非公共 API 函数应接受MonoError *参数出错时调用 mono-error-internals.h 中的某个mono_error_set_*函数设置错误在运行时内部检查错误使用is_ok (error)、mono_error_assert_ok (error)、goto_if_nok或return_if_nok/return_val_if_nok如果出错且需要处理错误调用mono_error_cleanup (error)释放资源。MonoError*通常是一次性one-shot的清理后需要调用mono_error_init_reuse重新初始化才能复用但这不推荐。推荐的错误处理模式是ERROR_DECL (local_error); possibly_failing_function (local_error); if (!is_ok (local_error)) { // 处理或忽略 mono_error_cleanup (local_error); } // 或者确信不会失败时 mono_error_assert_ok (local_error);从 mono-error-internals.h 可以看到丰富的mono_error_set_*函数族例如mono_error_set_error、mono_error_set_type_load_class、mono_error_set_out_of_memory、mono_error_set_argument_format、mono_error_set_argument_null、mono_error_set_generic_error、mono_error_set_execution_engine、mono_error_set_invalid_cast、mono_error_set_divide_by_zero、mono_error_set_null_reference等几乎覆盖了所有常见的托管异常类型。托管异常新代码一般不应直接处理MonoException*应改用MonoError*。同时应避免使用mono_error_set_pending_exception——它会以对调用者不明显的方式影响线程局部标志可能踩踏在途in-flight的异常导致代码以意外方式失败。仅以下两种场景需要调用mono_error_set_pending_exception正在使用会设置 pending exception 的公共 Mono API正在实现 icall但无法使用HANDLES()见下文内部调用一节。内部调用Internal Calls优先选择 P/Invoke 或 QCall而不是 internal calls。如果函数只接收非托管对象参数、且不需要与运行时交互最好在运行时之外定义。内部调用一般具有以下特征之一至少有一个参数是托管对象可能抛出托管异常。内部调用在 icall-def.h 中声明头文件内有详细注释说明用法。该文件使用ICALL_TYPE和ICALL宏描述每个 icall例如ICALL_TYPE(RTCLASS, Mono.RuntimeClassHandle, RTCLASS_1) NOHANDLES(ICALL(RTCLASS_1, GetTypeFromClass, ves_icall_Mono_RuntimeClassHandle_GetTypeFromClass))内部调用有两种风格NOHANDLES与HANDLES这是一个简化说法因为执行引擎还会添加 JIT internal callsHANDLESicall对托管对象的引用包裹在 handle 中即使线程阻塞或被挂起也能在内部调用期间保持对象存活同时会从托管到原生的互操作层获得一个MonoError*参数函数返回时转换为托管异常。NOHANDLES函数没有上述保护通常必须自行调用mono_error_set_pending_exception。HANDLES的声明示例来自 icall-def.h 的实际代码HANDLES(ARRAY_6, GetLengthInternal, ves_icall_System_Array_GetLengthInternal, gint32, 2, (MonoObjectHandleOnStack, gint32))挂起安全Suspend SafetyMono 采用协作式挂起cooperative suspend模型。可以参阅 mono 线程状态机设计文档 了解详情。基本规则可能从公共 Mono API 调用的运行时函数如果调用了其他运行时 API应包裹在MONO_ENTER_GC_UNSAFE/MONO_EXIT_GC_UNSAFE中内部调用icall进入时已处于 GC Unsafe 状态但 QCalls 和 P/Invoke 不是需要自行处理调用阻塞式原生 API 时应将调用包裹在MONO_ENTER_GC_SAFE/MONO_EXIT_GC_SAFE中。在同步原语的选择上优先使用mono_coop_同步原语MonoCoopMutex、MonoCoopCond、MonoCoopSemaphore等——它们在执行可能阻塞的操作前会自动进入 GC Safe 模式这正是上文 mono-coop-mutex.h 中mono_coop_mutex_lock的实现行为mono_os_原语mono_mutex_t、mono_cond_t只应用于底层叶锁leaf locks这类锁可能需要与非运行时代码共享使用它们时需要自行负责在执行阻塞操作前切换到 GC Safe 模式。相关的底层 API 位于 mono-threads-api.h例如mono_threads_enter_gc_unsafe_region、mono_threads_enter_gc_safe_region及其非平衡unbalanced变体协作式挂起的策略与安全点逻辑则在 mono-threads-coop.h如mono_threads_are_safepoints_enabled、mono_threads_safepoint与 mono-threads-state-machine.c 中实现。GC 内存安全Mono 团队探索过多种从运行时安全访问托管内存的策略现有代码并不统一。当前策略如下注意本文档可能需更新请与团队成员确认绝不允许在 GC Safe 代码中访问托管对象Mono 的 GC 精确扫描托管堆不允许堆对象包含指向其他托管对象内部的指针Mono 的 GC 保守扫描原生栈任何看起来像指向 GC 堆的指针的值都会导致目标对象被固定pin而不会被回收。栈上指向托管对象内部的内部指针interior pointers是允许的且会固定该对象。保持对象存活的机制特别是跨从原生回调托管或可能触发 GC 的调用实际上几乎任何运行时内部 API 都可能因程序集加载触发托管回调时应使用以下机制之一确保对象存活在托管代码中用fixed固定对象常见于字符串原生代码获得gunichar2*将ref局部变量传入原生代码原生代码获得MonoObject * volatile *volatile很重要将SpanT传入原生代码原生代码获得MonoSpanOfObjects*使用HANDLES()声明 icall传入MonoObjectHandle或更具体的类型如MonoReflectionTypeHandle传入 GCHandle。通常只有托管与原生边界上的函数才需要使用上述机制之一即对象被固定一次即可被调用方可以接受MonoObject*参数并假定调用方已固定该对象。原生代码中创建的对象当对象在原生代码中创建时应通过以下方式保持其存活赋值到MonoObject * volatile *即在 C# 中使用out或ref参数使用MONO_HANDLE_NEW或MONO_HANDLE_PIN创建本地句柄函数随后应使用HANDLE_FUNCTION_ENTER/HANDLE_FUNCTION_RETURN建立和拆除句柄帧创建 GCHandle赋值到另一个托管对象的字段中。在所有情况下如果存在任何中间内部 API 调用或对托管代码的调用必须在调用前使用上述方法之一保持对象存活。写屏障Write barriers当向另一个托管对象的字段写入托管对象时应使用mono_gc_wbarrier_系列函数例如mono_gc_wbarrier_generic_store。如果目标不在托管堆中调用写屏障函数也是可以的此时它们只是执行普通写入。断言Mono 代码应使用g_assert、mono_error_assert_ok、g_assertf、g_assert_not_reached等。与 CoreCLR 不同Mono 的断言始终包含在运行时中——无论是 Debug 还是 Release 构建。新代码应尽量避免依赖断言条件的副作用因为未来可能希望在 Release 构建中关闭断言。Mono 公共 APIMono 为嵌入 Mono 运行时的项目维护一套公共 API使它们能够在其他应用或框架的上下文中执行 .NET 代码。当前 Mono API 版本为mono-2.0目标是保持对嵌入方embedders的二进制 ABI 和 API 稳定性。公共 API 头文件定义在 src/native/public/mono。修改其中声明的任何函数都需要格外小心不允许破坏 ABI删除函数或更改参数应避免语义变更或以实现对嵌入方干扰最小的方式实施例如运行时已不再支持多个 AppDomain但mono_domain_get仍然继续工作。公共 API 头文件按领域组织在 src/native/public/mono/metadata 下包括appdomain.h、assembly.h、class.h、image.h、object.h、threads.h、exception.h、profiler.h等。隐藏的 API 函数实际上某些函数即使未在公共头文件中声明也被标记了MONO_API。这些符号已被嵌入 Mono 的项目使用应像真正的公共 API 一样谨慎对待。不稳定 API 函数在mono-private-unstable.h头文件中声明的函数不需要维护 API/ABI 稳定性。它们通常是 .NET 5 之后新增但尚未稳定的 API。作为礼貌如果更改这些函数的行为请通知 .NET macios 和 .NET Android 团队。WASMsrc/mono/browser/runtime 中的 WASM 与 WASI 运行时实际上是外部 API 客户端应尽可能使用现有的MONO_API函数。出于方便wasm 项目有时会利用静态链接的优势在 src/mono/browser/runtime/driver.c 中声明内部 Mono 函数并直接调用运行时内部实现。新代码一般不应这样做。在修改现有代码时要注意神秘的 WASM 失败可能源于 WASM 与 Mono 运行时之间的符号签名不匹配symbol signature mismatches。快速自查清单提交代码前可以用以下清单快速检查是否符合本指南缩进使用 Tab函数名与(之间留空格{与关键字同行前缀非 static 符号使用mono_/monovm_/m_/monoeg_前缀公共 API 必须加前缀类型优先标准 C 定长类型布尔类型按场景选用gboolean/MonoBoolean/mono_bool宏区分HOST_XYZ与TARGET_XYZ宏使用MONO_前缀C11用static_assert而非_Static_assert不使用标准 C 线程锁使用mono_atomic_*而非 C 标准原子目录依赖遵守 eglib → utils → sgen → metadata → mini 的依赖层级错误处理用MonoError*mono_error_set_*is_ok/mono_error_assert_ok避免mono_error_set_pending_exceptionGC 安全阻塞调用包MONO_ENTER_GC_SAFE/MONO_EXIT_GC_SAFE访问运行时 API 包MONO_ENTER_GC_UNSAFE/MONO_EXIT_GC_UNSAFE优先mono_coop_同步原语托管内存GC Safe 代码绝不访问托管对象对象跨调用存活使用 handle/ref/fixed/GCHandle字段写入使用mono_gc_wbarrier_系列断言使用g_assert系列但不要依赖断言副作用公共 API保持 ABI 稳定mono-private-unstable.h中的函数才允许自由变更。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考