ModFramework.ByRefUtils 1.0.4

dotnet add package ModFramework.ByRefUtils --version 1.0.4
                    
NuGet\Install-Package ModFramework.ByRefUtils -Version 1.0.4
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ModFramework.ByRefUtils" Version="1.0.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ModFramework.ByRefUtils" Version="1.0.4" />
                    
Directory.Packages.props
<PackageReference Include="ModFramework.ByRefUtils" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ModFramework.ByRefUtils --version 1.0.4
                    
#r "nuget: ModFramework.ByRefUtils, 1.0.4"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ModFramework.ByRefUtils@1.0.4
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ModFramework.ByRefUtils&version=1.0.4
                    
Install as a Cake Addin
#tool nuget:?package=ModFramework.ByRefUtils&version=1.0.4
                    
Install as a Cake Tool

ByRefUtils

C# (.NET Core / Standard 2.0) utilities for variable references ( ref T val )

What's this project for

In C# 7 we got new feature: ref local and ref return.

But still, many things with "ref" cannot be done yet.

For example, we cannot declare a field in a class with type "ref int".

class WeWantThisClass
{
    public ref int Reference;
}

And we want to check whether 2 reference are equal.

public static bool ReferenceEquals(ref int a, ref int b)

And sometimes, we want to ref return null.

public static ref int Find(IList<int> list, int val)
{
    //...
    // if we cannot find?
    ref return null;
}

So I created the ByRefUtils.dll to help us to do these.

How to use

  1. Download and Copy the ByRefUtils.dll to your project.
  2. Add Reference to ByRefUtils.dll
  3. Use it.

What can it do

  1. using Mod.LowLevel;

  2. Declare a ref to a variable

        int i = 0;
        var r = new RawRef();
        r.SetRef(ref i);
        // Or
        var r2 = RawRef.Of(ref i);

RawRef is a struct. You can also use Mod.LowLevel.Ref (class) or Mod.LowLevel.Ref<T> (class)

  1. Get ref from RawRef / Ref / Ref<T>
        RawRef r;
        //...
        ref int ri = ref r.GetRef<int>();
  1. Get / Set Value
        int i = 0;
        var r = RawRef.Of(ref i);
        r.SetValue(2);
  1. Get empty (null) ref
        Ref.GetEmptyRef<T>()
        // or new RawRef / Ref / Ref<T> and GetRef from them
  1. Check ref equals
        Ref.RefEquals<T>(ref T a, ref T b)
  1. Check a ref is empty (null)
        // check ref equals to GetEmptyRef
        Ref.RefEquals<T>(ref T a, ref Ref.GetEmptyRef<T>())
        // or
        Ref.IsEmpty<T>(ref T r)
  1. We can do dangerous convert use it
        int i = 1;
        var r = RawRef.Of(ref i);
        var plat = r.GetRef<RuntimePlatform>(); // RuntimePlatform is an enum
  1. Takes an object as a ref!
        object o = new object();
        var r = RawRef.Of(o);
        var address = r.Address;
  1. Get writable ref from a readonly ref.
        int i = 1;
        ref int r = Ref.Unprotect(in i);

Remarks

  1. CLR GC will move objects at managed heap, so holding a ref to an address at heap for a long time is dangerous. In this case, you should use TrackingRef instead.
  2. Declaring a ref to a variable at stack is safe. But we should notice memory layout.
  3. It is tested in .NET Core 3.0 and Unity (both Mono and IL2CPP)
  4. We cannot implement this dll in C#, but IL can. Some of the code is injected with Mono.Cecil

TrackingRef

Because objects on gc heap will be moved by garbage collector, so if you need a ref pointing to an object's field, you should use TrackingRef instead of Ref.

How to use

  1. Import and reference the ByRefUtils.TrackingRef.dll
  2. Use Mod.LowLevel.RawTrackingRef (struct) or Mod.LowLevel.TrackingRef (class) or Mod.LowLevel.TrackingRef<T> (class). You can use them just like RawRef/Ref.
  3. Donot forget to Dispose TrackingRef.

About the trick to implement TrackingRef

After gc moved the object, gc will auto change any ref on execution stack to the correct address. So we can make a new thread and make ref locals on this thread's execution stack. We use RawRef to get the address of locals on thread's stack and read real address from it.

LocalRef - Light weight TrackingRef

Creating TrackingRef is expensive. We can point to a ref variable on running evaluation stack using LocalRef. It can track object moving and do not need Dispose. It is useful to pass a unpinned ref to native child method, when the native method donot really read from the address. (Mostly, the native method will callback to C# to reflect call a managed method)

The Visual Studio Solution

  1. Compile and Run Generator project (in Release mode) to generate ByRefUtils.dll and ByRefUtils.TrackingRef.dll
  2. Compile and Run TestByRefUtils project to make a test.

Performance

The TestByRefUtils project contains a simple test. The result is likely this:

Performance test - ref keyword:
ref keyword: 0 ms
Performance test - RawRef:
RawRef: 0 ms
Performance test - LocalRef:
LocalRef: 0 ms
Performance test - TrackingRef:
TrackingRef: 0 ms
Performance test - create ref keyword:
ref keyword: 1 ms
Performance test - create RawRef:
RawRef: 2 ms
Performance test - create TrackingRef:
TrackingRef: 341 ms

So, it almost cost no overhead when accessing through RawRef or TrackingRef. But creating and Disposing TrackingRef is a bit expensive.

中文说明

这个工程是做什么的?

在 C# 7.0 中,我们获得了新特性:ref 局部变量和返回值。

但是仍有许多限制阻碍着我们更自然地使用 "ref"。

比如,C#不允许我们声明 ref 的字段。比如下面这个类型就是非法的。

class WeWantThisClass
{
    public ref int Reference;
}

有时,我们想比较两个引用的内存地址是否相等,就像 object.ReferenceEquals 一样。

public static bool ReferenceEquals(ref int a, ref int b)

有时,我们想返回一个为空的地址。比如在集合中进行查找,没有找到时。

public static ref int Find(IList<int> list, int val)
{
    //...
    // 如果找不到怎么办?
    ref return null;
}

因此我创建了一个程序集(.NET standard 2.0)来解决上述的需求。

怎样使用呢?

  1. 下载并拷贝 ByRefUtils.dll 到目标工程。
  2. 在目标工程中添加 ByRefUtils.dll 的引用。
  3. 使用 ByRefUtils.dll 中的类型。(集中在 Mod.LowLevel 命名空间中)

代码示例

  1. using Mod.LowLevel;

  2. 声明一个变量的引用

        int i = 0;
        var r = new RawRef();
        r.SetRef(ref i);
        // Or
        var r2 = RawRef.Of(ref i);

RawRef 是值类型(struct). 也可以使用 Mod.LowLevel.Ref (引用类型class) or Mod.LowLevel.Ref<T> (泛型引用类型)

  1. RawRef / Ref / Ref<T> 里拿引用
        RawRef r;
        //...
        ref int ri = ref r.GetRef<int>();
  1. 取值/赋值
        int i = 0;
        var r = RawRef.Of(ref i);
        r.SetValue(2);
  1. 拿一个空引用
        Ref.GetEmptyRef<T>()
        // 也可以 new RawRef / Ref / Ref<T> 然后直接调用 GetRef
  1. 检查引用的地址是否相等
        Ref.RefEquals<T>(ref T a, ref T b)
  1. 检查一个引用是否为空
        // 与 GetEmptyRef 进行引用比等
        Ref.RefEquals<T>(ref T a, ref Ref.GetEmptyRef<T>())
        // 或者调用这个函数
        Ref.IsEmpty<T>(ref T r)
  1. 我们可以使用它来进行快速(也危险)的类型转换
        int i = 1;
        var r = RawRef.Of(ref i);
        var plat = r.GetRef<RuntimePlatform>(); // RuntimePlatform 是一个枚举
  1. 将一个对象转换为指向它堆上位置的引用
        object o = new object();
        var r = RawRef.Of(o);
        var address = r.Address;
  1. 将一个只读引用(in),转换为普通引用
        int i = 1;
        ref int r = Ref.Unprotect(in i);

说明

  1. 运行时的垃圾回收机制有可能移动托管堆上的对象,因此长时间持有一个指向托管堆的引用是危险的,这种情况下,请使用下面的TrackingRef。
  2. 声明一个指向栈上的引用一般是安全的。但是要确定尽量不要访问栈顶之上的内存(如果是简单值类型引用似乎也没啥危险),以及进行类型转换时一定要确定值的内存大小和排列是兼容的。(比如对一个int的枚举使用ulong的引用去转的话,在小端系统上没啥问题,在大端系统上就不正确)
  3. 在 .NET Core 3.0 和 Unity 中进行过测试 (Mono 和 IL2CPP 都进行过测试)。
  4. 这个程序集纯用C#是无法实现的,但是IL(中间语言)是支持这些操作的,所以这个程序集才能做出来。部分代码是通过 Mono.Cecil 来编辑 IL 得到的。

TrackingRef

因为运行时的垃圾回收机制有可能移动托管堆上的对象,如果想引用堆上对象的字段,应该使用TrackingRef来代替Ref。

怎样使用

  1. 导入并引用 ByRefUtils.TrackingRef.dll
  2. 使用 Mod.LowLevel.RawTrackingRef (struct) 或 Mod.LowLevel.TrackingRef (class) 或 Mod.LowLevel.TrackingRef<T> (class) 代替 RawRef / Ref / Ref<T>
  3. 使用TrackingRef / RawTrackingRef后一定要记得 Dispose()!!

实现原理

当gc移动了堆上object之后,它会检查调用栈上是否有ref引用到这个object的字段,并自动将这些ref引用的地址更正为新的值。在TrackingRef中新建了一个线程来维护一个调用栈,这个线程的栈上基本全是ref局部变量。然后通过RawRef来获取线程栈上局部变量的地址(ref局部变量的地址,相当于指针的指针),然后从中读取一个IntPtr,就是真正的地址了。

LocalRef - TrackingRef的轻量替代

创建TrackingRef是较费时的,只适用与想长期持有这个引用的情况。我们可以拿到一个执行栈上局部ref变量的地址做简介引用,这就是LocalRef。它也能追踪GC对对象的移动,而且不需要Dispose这个LocalRef。常用于调用一个本地方法,这个本地方法会通过反射的方式回调一个托管方法,并且要传递一个ref作为方法参数,这个本地方法并不是直接去读引用地址,也不需要固定这个对象,只是作为一个中间层,透传一个ref变量而已。

Visual Studio工程

  1. 编译并运行 Generator 工程 (最好是 Release) 来生成 ByRefUtils.dll 与 ByRefUtils.TrackingRef.dll
  2. 然后编译并执行 TestByRefUtils 工程来进行一下测试

性能

TestByRefUtils工程包含一个简单的测试。测试结果大致如下:

Performance test - ref keyword:
ref keyword: 0 ms
Performance test - RawRef:
RawRef: 0 ms
Performance test - LocalRef:
LocalRef: 0 ms
Performance test - TrackingRef:
TrackingRef: 0 ms
Performance test - create ref keyword:
ref keyword: 1 ms
Performance test - create RawRef:
RawRef: 2 ms
Performance test - create TrackingRef:
TrackingRef: 341 ms

因此,可以认为通过RawRef或者TrackingRef进行数据访问是没有额外开销的。但是要注意,建立和销毁TrackingRef是一个比较耗时的操作。

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.4 95 6/1/2026
1.0.3 95 5/27/2026
1.0.2 92 5/26/2026
1.0.1 96 5/11/2026
1.0.0 92 5/11/2026