C# delegate의 내부 구조에 대하여 I (SingleCast)

[제목] C# delegate의 내부 구조에 대하여 I (SingleCast)

C# delegate는 메서드에 대한 레퍼런스를 Encapsulation하여 이를 다른 곳으로 전달하거나 호출할 수 있는 기능을 제공한다. C#의 delegate는 기본적으로 Multicast 기능을 가지고 있으며, .NET의 MulticastDelegate 클래스를 기반으로 하고 있다.

C# delegate는 생성시 MulticastDelegate 클래스로부터 파생된 클래스의 객체 인스턴스를 생성하는데, 이 객체의 내부 구조를 간략히 살펴보는 것은 델리게이트를 이해하는 도움이 될 수 있다.

우선 아래와 같이 간단한 SingleCast Delegate 예제를 살펴보자. 이 예제에서는 (1) MyDelegate 라는 델리게이트를 정의하고, (2) 델리게이트 인스턴스를 생성한 후 (3) 이를 실행하는 것을 보여 준다. C# delegate는 Static 메서드(ex: StaticRun())를 가리킬 수도 있고, Instance 메서드(ex: InstRun())을 가리킬 수도 있다.

namespace DeleApp
{
    class Program
    {
	    //1. 델리게이트 정의
        public delegate void MyDelegate();

        static void Main(string[] args)
        {
            Class1 c = new Class1();

			//2. 델리게이트 인스턴스 생성
            MyDelegate m1 = new MyDelegate(c.InstRun);
            MyDelegate m2 = new MyDelegate(Class1.StaticRun);

            Console.ReadLine();
			
			//3. 델리게이트 실행
            m1();
            m2();
        }
    }

    class Class1 
    {
        public void InstRun()
        {
            Console.WriteLine("InstRun");
        }

        public static void StaticRun()
        {
            Console.WriteLine("StaticRun");
        }
    }
}

C# delegate가 생성하는 델리게이트 객체를 살펴보기 위해 위의 실행 코드를 Windbg 디버거를 통해 살펴보자. Console.ReadLine() 에서 중지하고 !clrstack 을 실행하면 맨 마지막에 아래와 같은 Main() 메서드의 로컬 변수들을 볼 수 있다.

0:000> .load sos
0:000> !clrstack -a
...생략...
0050f358 003200f2 DeleApp.Program.Main(System.String[]) [c:\Dev\DeleApp\Program.cs @ 19]
    PARAMETERS:
        args (0x0050f374) = 0x02492250
    LOCALS:
        0x0050f370 = 0x02492260
        0x0050f36c = 0x0249226c
        0x0050f368 = 0x0249228c

첫번째 로컬변수의 값(0x02492260)을 !dumpobj (줄여서 !do)로 살펴보면, 이것이 Class1 객체라는 것을 알 수 있다.

0:000> !do 0x02492260
Name:        DeleApp.Class1
MethodTable: 001c3828
EEClass:     001c1300
Size:        12(0xc) bytes
File:        C:\Dev\DeleApp\bin\Debug\DeleApp.exe
Fields:
None

다음 2번째 로컬변수 값(0x0249226c)을 보면, 이는 MyDelegate 델리게이트 클래스 객체이고 6개의 필드를 가지고 있는 것을 알 수 있다.

// MyDelegate m1 = new MyDelegate(c.InstRun);

0:000> !do 0x0249226c
Name:        DeleApp.Program+MyDelegate
MethodTable: 001c38d0
EEClass:     001c1354
Size:        32(0x20) bytes
File:        C:\Dev\DeleApp\bin\Debug\DeleApp.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
5d44b064  400002d        4        System.Object  0 instance 02492260 _target   ==> Class1 obj
5d44b064  400002e        8        System.Object  0 instance 00000000 _methodBase
5d44a0e8  400002f        c        System.IntPtr  1 instance   1cc078 _methodPtr    ==> method ref
5d44a0e8  4000030       10        System.IntPtr  1 instance        0 _methodPtrAux
5d44b064  4000031       14        System.Object  0 instance 00000000 _invocationList
5d44a0e8  4000032       18        System.IntPtr  1 instance        0 _invocationCount

델리게이트 객체는 하나의 메서드 레퍼런스를 갖는 SingleCast인 경우와 복수개의 메서드 레퍼런스를 갖는 MultiCast의 경우가 있는데, 각 경우에 따라 서로 다른 필드들을 사용한다. 즉, SingleCast의 경우는 첫번째 4개의 필드를 사용하고, MultiCast인 경우 마지막 2개의 필드를 사용한다.

SingleCast의 경우, 여기서 첫번째 _target 필드는 Instance 메서드를 Encapsulation할 때, 해당 메서드의 객체 인스턴스를 갖게된다. 만약 Static 메서드인 경우는 델리게이트 객체 자신을 지정하기 때문에 큰 의미는 없다. Instance 메서드인 경우 3번째 필드 _methodPtr는 메서드 레퍼런스값을 의미한다. 이 주소값(1cc078)으로부터 실제 Method Description을 얻기 위해서는 [주소값+8] 어드레스에 위치한 값을 포인터값을 poi() 사용하여 Method Descriptor 주소를 얻은 후, 이 주소에 대해 SOS의 !dumpmd 명령을 사용하면 된다. 이를 통해 아래 예에서는 해당 위치에 Class1.InstRun() 메서드가 있음을 알 수 있다.

0:000> !dumpmd poi(1cc078+8)
Method Name:  DeleApp.Class1.InstRun()
Class:        001c1300
MethodTable:  001c3828
mdToken:      06000008
Module:       001c2e94
IsJitted:     no
CodeAddr:     ffffffff
Transparency: Critical

델리게이트 객체가 Instance Method를 Encapsulation 한다는 것은 메서드 레퍼런스와 함께 객체 인스턴스도 함께 전달한다는 것을 의미한다. 즉, 만약 인스턴스 메서드에서 해당 인스턴스의 어떤 필드를 사용해야 한다면, 메서드 레퍼런스 뿐만 아니라 그 인스턴스도 필요할 텐데, C# delegate 객체는  델리게이트 객체 자체에서 두가지 필요한 정보를 모두 포함하고 있기 때문에 델리게이트 밖에서 별도의 인스턴스 객체를 파라미터로 전달할 필요가 없다.

만약 Static 메서드인 경우, _target은 MyDelegate객체 자신을 가리키므로 큰 의미는 없고, 4번째 필드 _methodPtrAux를 살펴보아야 한다. 이번에는 Instance 메서드와 다르게 메서드 레퍼런스 값이 _methodPtrAux필드에 있게 된다.

// MyDelegate m2 = new MyDelegate(Class1.StaticRun);

0:000> !do /d 0x0249228c
Name:        DeleApp.Program+MyDelegate
MethodTable: 001c38d0
EEClass:     001c1354
Size:        32(0x20) bytes
File:        C:\Dev\DeleApp\bin\Debug\DeleApp.exe
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
5d44b064  400002d        4        System.Object  0 instance 0249228c _target  ==> MyDelegate itself
5d44b064  400002e        8        System.Object  0 instance 00000000 _methodBase
5d44a0e8  400002f        c        System.IntPtr  1 instance   7e07ec _methodPtr
5d44a0e8  4000030       10        System.IntPtr  1 instance   1cc088 _methodPtrAux  ==> method ref
5d44b064  4000031       14        System.Object  0 instance 00000000 _invocationList
5d44a0e8  4000032       18        System.IntPtr  1 instance        0 _invocationCount

마찬가지로 _methodPtrAux (1cc088)을 !dumpmd로 살펴보면 이것이 Class1.StaticRun()을 가리키고 있음을 알 수 있다.

0:000> !dumpmd poi(1cc088+8)
Method Name:  DeleApp.Class1.StaticRun()
Class:        001c1300
MethodTable:  001c3828
mdToken:      06000009
Module:       001c2e94
IsJitted:     no
CodeAddr:     ffffffff
Transparency: Critical

지금까지는 SingleCast 델리게이트의 경우를 살펴보았다. 이어 다음 아티클에서는 MultiCast 델리게이트의 내부구조에 관해 살펴보도록 하겠다...

아티클 : C# delegate의 내부 구조에 대하여 II (MultiCast)
 



본 웹사이트는 광고를 포함하고 있습니다. 광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.