.NET靜態程式碼織入——肉夾饃(Rougamo)釋出3.0

nigture發表於2024-05-06

肉夾饃(https://github.com/inversionhourglass/Rougamo)透過靜態程式碼織入方式實現AOP的元件,其主要特點是在編譯時完成AOP程式碼織入,相比動態代理可以減少應用啟動的初始化時間讓服務更快可用,同時還能對靜態方法進行AOP操作。

正文

雖又是一個大版本,但本次大版本沒有重大的功能上線,主要是修改了程式碼織入方式,這樣的改動牽扯到一些現有功能。

程式碼織入方式變化

在3.0之前的版本中採用的是程式碼內嵌的方式進行織入,下面用簡化程式碼進行演示:

// 原始方法
public void M()
{
    Console.WriteLine(1);
    Console.WriteLine(2);
    Console.WriteLine(3);
}

// 3.0版本之前織入程式碼後
public void M()
{
    var context = new MethodContext(...);
    var mo = new AbcAttribute();
    mo.OnEntry(context);
    try
    {
        Console.WriteLine(1);
        Console.WriteLine(2);
        Console.WriteLine(3);
        mo.OnSuccess(context);
    }
    catch (Exception e)
    {
        context.Exception = e;
        mo.OnException(context);
        throw;
    }
    finally
    {
        mo.OnExit(context);
    }
}

在3.0版本中採用的是代理呼叫的方式進行程式碼織入,將原方法複製為一個新方法$Rougamo_M,然後修改原方法進行程式碼織入後呼叫$Rougamo_M,簡化程式碼如下:

// 將原方法M複製為$Rougamo_M
public void $Rougamo_M()
{
    Console.WriteLine(1);
    Console.WriteLine(2);
    Console.WriteLine(3);
}

// 修改原方法進行程式碼織入並呼叫複製後的原方法
public void M()
{
    var context = new MethodContext(...);
    var mo = new AbcAttribute();
    mo.OnEntry(context);
    try
    {
        $Rougamo_M();
        mo.OnSuccess(context);
    }
    catch (Exception e)
    {
        context.Exception = e;
        mo.OnException(context);
        throw;
    }
    finally
    {
        mo.OnExit(context);
    }
}

不同織入方法帶來的影響

ExMoAttribute的棄用

早在1.2版本中新增了ExMoAttribute,可能很多朋友對此都並不瞭解,ExMoAttribute主要用來遮蔽使用和不使用async/await語法所帶來的差異,因為使用async/await語法後,在編譯時會生成對應的狀態機型別,那麼肉夾饃就會對應修改狀態機程式碼進行織入,而不使用async/await語法的方法就只能對原方法進行程式碼織入,下面用程式碼簡單演示其中的差異:

public async Task Delay()
{
    Console.WriteLine(1);
    await Task.Delay(2000);
    Console.WriteLine(2);
}

// 使用async/await語法呼叫Delay
public async Task WithSyntax()
{
    var context = new MethodContext(...);
    var mo = new AbcAttribute();
    mo.OnEntry(context);
    try
    {
        await Delay();

        mo.OnSuccess(context);
    }
    catch (Exception e)
    {
        context.Exception = e;
        mo.OnException(context);
        throw;
    }
    finally
    {
        mo.OnExit(context);
    }
}

// 不使用async/await語法呼叫Delay
public Task WithoutSyntax()
{
    var context = new MethodContext(...);
    var mo = new AbcAttribute();
    mo.OnEntry(context);
    try
    {
        var task = Delay();

        mo.OnSuccess(context);

        return task;
    }
    catch (Exception e)
    {
        context.Exception = e;
        mo.OnException(context);
        throw;
    }
    finally
    {
        mo.OnExit(context);
    }
}

在上面的程式碼示例中,沒有使用async/await的WithoutSyntax方法會在Delay還沒有執行完畢之前就執行OnSuccessOnExit方法。

ExMoAttribute針對沒有使用async/await語法的方法透過在OnSuccess方法中使用ContinueWith達到在非同步方法實際執行完畢後執行OnExit系列方法。ExMoAttribute雖然能夠解決語法差異帶來的問題,但也增加了一定的複雜性,同時因為其可能鮮為人知,所以在使用時因語法差異帶來的問題可能後知後覺。

在3.0版本中由於使用代理呼叫的方式,被代理方法是否使用async/await語法是被遮蔽的,代理方只需要知道你的返回值是Task即可,所以在3.0版本中MoAttribute即可應對是否使用async/await語法的兩種情況,ExMoAttribute在3.0版本中標記為Obsolete並將在4.0版本中直接刪除。下面用程式碼簡單說明3.0對於是否使用async/await語法的統一處理方式:

// 複製WithSyntax原方法為$Rougamo_WithSyntax
public async Task $Rougamo_WithSyntax()
{
    await Delay();
}

// 複製WithoutSyntax原方法為$Rougamo_WithoutSyntax
public Task $Rougamo_WithoutSyntax()
{
    return Delay();
}

public async Task WithSyntax()
{
    // ...程式碼織入
    try
    {
        await $Rougamo_WithSyntax();
    }
    catch
    {
        // ...程式碼織入
    }
}

public async Task WithoutSyntax()
{
    // ...程式碼織入
    try
    {
        await $Rougamo_WithoutSyntax();
    }
    catch
    {
        // ...程式碼織入
    }
}

async void 弱支援

如果說前面介紹的是織入方式改變帶來的優勢,那麼這裡介紹的就是劣勢了。async void方法是一種特殊的非同步方法,同樣會生成對應的狀態機型別,但呼叫該方法無法進行await操作,也就無法等待該方法實際執行完畢。在3.0版本之前,由於採取的是內嵌程式碼織入,直接修改狀態機程式碼完成織入,所以OnExit系列方法可以在正確的時間點執行,而在3.0版本後由於採用了代理呼叫的方式,所以在執行OnExit系列方法時無法確保方法實際已經執行完畢。

關於async void的織入方式目前還在思考中,考慮到winform和wpf中可能存在不少的async void寫法,代理呼叫的織入方式可能就無法滿足目前的使用要求了,所以我將在github中釋出一個issue進行投票統計,請日常開發涉及到async void的朋友移步到github( https://github.com/inversionhourglass/Rougamo/issues/68 )中進行投票,投票將在4.0版本開發末期截止。

支援步入除錯

在3.0版本之前,應用了肉夾饃完成織入的方法在開發時無法進行步入除錯,這是因為之前的版本沒有對除錯資訊做對應的修改,沒有去做這一功能也是因為比較複雜懶得整。在3.0修改程式碼織入方式後,修改對應的除錯資訊相對要簡單許多,因此3.0版本支援步入除錯

僅ref/out支援重新整理引數

在2.1版本中新增重新整理引數功能,支援在OnSuccess / OnException / OnExit中透過MethodContext.Arguments獲取最新的引數值,但在3.0版本之後,由於織入程式碼方式的改變,此功能僅支援refout引數。

public void M(int x, out decimal y, ref string z)
{
    // ...
}

// 3.0 版本之前的織入方式
public void M(int x, out decimal y, ref string z)
{
    try
    {
        // ...
        // 由於是內嵌織入,在這裡可以直接獲取到所有最新的引數值,所以引數x也可以更新
        context.Arguments[0] = x;
        context.Arguments[1] = y;
        context.Arguments[2] = z;
        mo.OnSuccess(context);
    }
    catch (Exception e)
    {
        // ...
        // 由於是內嵌織入,在這裡可以直接獲取到所有最新的引數值,所以引數x也可以更新
        context.Arguments[0] = x;
        context.Arguments[1] = y;
        context.Arguments[2] = z;
        mo.OnException(context);
        throw;
    }
}

// 3.0 版本的織入方式
public void M(int x, out decimal y, ref string z)
{
    try
    {
        $Rougamo_M(x, out y, ref z);
        // 由於是代理呼叫織入,引數x在$Rougamo_M中被重新賦值後無法在外部獲取,所以僅更新引數y和z
        context.Arguments[1] = y;
        context.Arguments[2] = z;
        mo.OnSuccess(context);
    }
    catch (Exception e)
    {
        // ...
    }
}

構造方法織入方式不變

由於構造方法較為特殊,readonly欄位僅可在構造方法中初始化,所以無法使用代理呼叫的織入方式,這也表示使用肉夾饃程式碼織入的構造方法無法支援步入除錯。

織入方式切換

新的編織方式涉及眾多程式碼,程式碼織入部分的程式碼近乎重寫,雖然做了大量的測試,但為了保證穩定性提供了降級配置,修改專案中FodyWeavers.xml檔案中Rougamo節點配置,透過設定proxy-calling="false",將織入方式改回3.0版本之前的內嵌織入方式。需要注意的是,該配置僅為過渡配置,將在4.0版本中移出並最終僅保留代理織入的方式,如果代理織入的方式存在任何問題,請及時反饋。

<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Rougamo proxy-calling="false" />
</Weavers>

其他更新

以下列出3.0版本相關的所有issue,有興趣的可以直接移步github檢視issue中的回覆

  • #36 應用Rougamo的方法支援步入除錯
  • #54 解決snupkg報checksum錯誤的問題,需直接依賴Fody,詳見issue回覆
  • #60 支援自定義AsyncMethodBuilder
  • #63 支援泛型Attribute
  • #65 修復特定Type型別無法作為MoAttribute構造方法引數

相關文章