はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】C#12から登場したInterceptorsを利用してコンパイル時に実行するメソッドを置き換える(主にSourceGeneratorと組み合わせる)

はじめに

C#12から登場したInterceptorsというコンパイル時に実行するメソッドを置き換えられる機能について紹介したいと思います。

github.com

ただしまだ実験的な機能なので、変更される可能性もありますし情報もまだそこまで出てきていません。注意してください。

確認環境

Rider2023.3 EAP8
.Net8.0
C#12

概要

Interceptorsを利用することでコンパイル時に特定のメソッドの呼び出しを別のメソッド呼び出しに置き換えることができます。正直割とやりたい放題が可能になる、かなり強力な機能だと思います。

Interceptors allow specific method calls to be rerouted to different code. Attributes specify the actual source code location so interceptors are generally appropriate only for source generators. You can read the interceptors proposal to learn more about how interceptors work.

// DeepL翻訳
インターセプターは、特定のメソッド呼び出しを別のコードに迂回させることができます。属性は実際のソースコードの場所を指定するので、インターセプターは一般的にソースジェネレーターにのみ適しています。インターセプターがどのように機能するかについては、インターセプターに関する提案書を読んでください。

New C# 12 preview features - .NET Blog

Interceptorsを有効にする

Interceptorsは実験的な機能です。有効にするためには.csprojに書き込まれているPropertyGroupの中に以下を付け加えます。

<PropertyGroup>
   <Features>InterceptorsPreview</Features>
</PropertyGroup>


またnamespaceInterceptorsPreviewNamespacesに指定する必要もあるそうです。後述しますがInterceptsLocation属性を利用しているnamespaceを記述してください。(Rider君ならエラー文に書くべき文章を教えてくれます)

namespace Sampleの場合

<PropertyGroup>
   <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);Sample</InterceptorsPreviewNamespaces>
</PropertyGroup>

// エラー文
Error CS9137 : 'インターセプター' の実験的な機能は、この名前空間では有効になっていません。プロジェクトに '$(InterceptorsPreviewNamespaces);Sample' を追加します。

使い方

Source Generatorと一緒に使うことが基本ですが、分かりやすいようシンプルな使い方を見ていきます。

using System.Runtime.CompilerServices;

namespace Sample
{
    public static class Program
    {
        public static void Main(string[] args)
        {
            var c = new C();
            
            // 以下のメソッド呼び出しがコンパイル時に別のメソッド呼び出しに変更されている!?
            c.InterceptableMethod(1); // (12,15): "interceptor 1"
            c.InterceptableMethod(1); // (13,15): "other interceptor 1"
            c.InterceptableMethod(2); // (14,15): "other interceptor 2"
            
            // 以下は普通に呼び出し
            c.InterceptableMethod(1); // "interceptable 1"
        }
    }

    class C
    {
        public void InterceptableMethod(int param)
        {
            Console.WriteLine($"interceptable {param}");
        }
    }
    
    static class D
    {
        // lineとcharacterでメソッド名の最初を指定すると、以下のメソッドが呼ばれるように変更される
        // filePathなども直書きするというよりは、SourceGeneratorを利用して書き込むのが前提で作られているような気がする
        [InterceptsLocation("/Users/user/RiderProjects/ComsoleAppNet8_0/ComsoleAppNet8_0/Program.cs", line: 12, character: 15)]
        public static void InterceptorMethod(this C c, int param)
        {
            Console.WriteLine($"interceptor {param}");
        }

        [InterceptsLocation("/Users/user/RiderProjects/ComsoleAppNet8_0/ComsoleAppNet8_0/Program.cs", line: 13, character: 15)]
        [InterceptsLocation("/Users/user/RiderProjects/ComsoleAppNet8_0/ComsoleAppNet8_0/Program.cs", line: 14, character: 15)]
        public static void OtherInterceptorMethod(this C c, int param)
        {
            Console.WriteLine($"other interceptor {param}");
        }
    }
}

// 「シンボル"InterceptsLocation"を解決できません」とのエラーが出てきてしまうので自身で定義
// おそらく実験的な機能でなくなれば必要なくなるかと
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute { }
}

InterceptsLocationAttributeを定義する

コメントにも書きましたがシンボル"InterceptsLocation"を解決できませんとのエラーが出てきてしまうので自分で定義します。

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public sealed class InterceptsLocationAttribute(string filePath, int line, int character) : Attribute
    {
    }
}

メソッドを置き換える

InterceptsLocation属性を記述したメソッドを定義します。

[InterceptsLocation("/Users/user/RiderProjects/ComsoleAppNet8_0/ComsoleAppNet8_0/Program.cs", line: 12, character: 15)]
public static void InterceptorMethod(this C c, int param)
{
    Console.WriteLine($"interceptor {param}");
}

またインスタンスメソッドは実は内部的に引数だけでなく自身のインスタンスも渡されています。ですのでthisキーワードを利用してインスタンスを渡してあげてください。