C# 6.0 완벽 가이드/ 반영과 메타자료

Contents

  • 앞장에서 보았듯이, C# 프로그램 소스 코드를 컴파일하면 어셈블리가 만들어진다. 어셈블리는 컴파일된 코드와 메타자료(metadata) 그리고 기타 자원들로 구성된다. 컴파일된 코드와 메타자료를 실행시점에서 조사하는 것을 가리켜 반영(reflection)이라고 한다.
  • 어셈블리 안에 담긴 컴파일된 코드에는 원 소스 코드의 내용이 거의 다 들어 있다. 지역변수 이름이나 주석, 전처리기 지시문 등의 일부 정보는 컴파일 과정에서 사리지지만 그 나머지의 상당 부분은 반영 기능을 이용해서 접근할 수 있다.
    • 실제로 반영 기능을 이용해서 역컴파일러(decompiler)를 작성하는 것도 가능하다.
  • .NET Framework가 제공하는 그리고 C#을 통해서 노출되는 여러 서비스(동적 바인딩, 직렬화, 자료 바인딩, Remoting 등)는 메타자료가 있어야 작동한다.
    • 독자가 작성하는 프로그램 역시 메타자료를 활용할 수 있으며 심지어는 커스텀 특성을 이용해서 메타자료에 새로운 정보를 추가할 수도 있다.
    • 반영 API는 System.Reflection 이름공간에 들어 있다. System.Reflection.Emit 이름공간에 있는 클래스들을 이용하면 새 메타자료와 실행 가능한 IL(Intermediate Language; 중간 언어) 코드를 동적으로 생성하는 것도 가능하다.
  • 이번 장에서 뭔가를 ‘동적으로’ 수행한다는 것은 형식 안전성이 오직 실행시점에서만 강제되는 어떤 작업을 반영 긴으을 이용해서 수행하는 것을 뜻한다.
    • 구체적인 메커니즘과 기능성은 다르지만 원칙적으로 이는 C#의 dynamic 키워드를 통한 동적 바인딩과 비슷하다.
    • 둘을 비교하자면 동적 바인딩이 반영 기능보다 훨씬 사용하기 쉽다. 그리고 동적 바인딩은 동적 언어 상호운용성을 위해 DLR(Dynamic Language Runtime)을 활용한다.
    • 반영은 사용하기가 비교적 번거롭고 오직 CLR만 고려한다. 그러나 CLR로 할 수 있는 것의 관점에서 본다면 좀 더 유연하다.
    • 예컨대 반영 기능을 이용해서 형식들과 멤버들의 목록을 얻을 수 있고, 형식의 이름을 문자열로 지정해서 그 인스턴스를 생성할 수 있으며, 즉석에서 어셈블리를 구축할 수 있다.

형식의 반영과 활성화

Type 객체얻기

  • System.Type의 인스턴스는 형식의 메타자료를 대표한다. Type은 널리 쓰이는 형식이라서 System.Reflection 이름공간이 아니라 System 이름공간에 들어 있다.
  • System.Type의 인스턴스를 얻는 방법은 크게 두 가지이다. 하나는 임의의 객체에 대해 GetType을 호출하는 것이고 또 하나는 C#의 typeof 연산자를 사용하는 것이다.
Type t1 = DateTime.Now.GetType();  // 실행시점에서 얻은 Type 객체
Type t2 =  typeof(DateTime);  // 컴파일시점에서 얻은 Type 객체
  • typeof 연산자는 배열 형식과 제네릭 형식도 지원한다.
Type t3 = typeof(DateTime[]);  // 1차원 Array 형식
Type t4 = typeof(DateTime[,]);  // 2차원 Array 형식
Type t5 = typeof(Dictionary<int, int>);  // 닫힌 제네릭 형식
Type t6 = typeof(Dictionary<,>);  // 묶이지 않은 제네릭 형식
  • 또한 문자열 이름으로 Type 객체를 얻을 수도 있다. 형식이 속한 어셈블리에 대한 Assembly 객체가 있다면, 형식 이름으로 Assembly.GetType을 호출하면 된다.
Type t = Assembly.GetExecutingAssembly().GetType("Demos.TestProgram");
  • Assembly 객체가 없는 경우에는 형식의 어셈블리 한정 이름을 통해서 그 형식을 대표하는 Type 객체(이하 간단히 형식 객체)를 얻을 수 있다.
    • 어셈블리 한정 이름은 형식의 완전 한정 이름 다음에 어셈블리의 완전 한정 이름을 붙인 것이다.
    • 이때 암묵적으로 해당 어셈블리가 Assembly.Load(string)을 호출했을 떄와 마찬가지 방식으로 적재된다.
Type t = Type.GetType("System.Int32, mscorlib, Version=2.0.0.0, " + "Culture=neutral, PublicKeyToken=b77a5c5...");
  • System.Type 객체를 얻었다면 여러 속성을 통해서 형식의 이름, 어셈블리, 기반형식, 가시성 등에 접근할 수 있다. 예컨대 다음과 같다.
Type stringType = typeof(string);
string name = stringType.Name;  // String
Type baseType = stringType.BaseType;  // typeof(Object)
Assembly assem = stringType.Assembly;  // mscorlib.dll
bool isPublic = stringType.IsPublic;  // true
  • System.Type 인스턴스는 형식의 전체 메타자료로 가는 관문이라 할 수 있다. 또한 형식이 정의되어 있는 어셈블리로의 접근 통로이기도 하다.
  • System.Type은 추상 클래스이므로, 형식 객체에 대한 typeof 연산자는 Type의 특정 파생 클래스의 인스턴스를 돌려준다. CLR은 RuntimeType이라는 Type의 파생 클래스를 사용하는데, 이 파생 클래스는 mscorlib의 내부 클래스이다.

TypeInfo와 Windows 스토어 앱

  • Windows 스토어 프로파일은 Type의 멤버들 대부분을 숨기고 대신 TypeInfo라는 클래스를 통해서 그 멤버들을 노출한다. TypeInfo 인스턴스는 GetTypeInfo 메서드로 얻을 수 있다.
    • 따라서 앞의 예제를 Windows 스토어 앱에서 실행하려면 다음과 같이 코드를 수정해야 한다.
Type stringType = typeof(string);
string name = stringType.Name;
Type baseType = stringType.GetTypeInfo().BaseType; 
Assembly assem = stringType.GetTypeInfo().Assembly;
bool isPublic = stringType.GetTypeInfo().IsPublic;
  • TypeInfo는 보통의 .NET Framework에도 있다. 따라서 Windows 스토어 앱에서 실행되는 코드는 .NET Framework 4.5 이상을 대상으로 하는 데스크톱 응용 프로그램에서도 작동한다. TypeInfo는 멤버의 반영에 필요한 추가적인 속성들과 메서드들도 제공한다.
  • Windows 스토어 앱은 반영 기능이 제한적이다. 특히 Windows 스토어 앱에서는 형식의 비공용 멤버들에 접근할 수 없으며, System.Reflection.Emit 이름공간도 사용할 수 없다.

배열 형식 얻기

  • typeof가 배열 형식을 지원한다는 점은 앞에서 이미 말했다. 배열에 대한 형식 객체를 얻는 또 다른 방법은 원소 형식에 대해 MakeArrayType을 호출하는 것이다.
Type simpleArrayType = typeof(int)MakeArrayType();
Console.WriteLine(simpleArrayType == typeof(int[]));  // True
  • 다차원 사각형 배열 형식을 만들려면 다음 예처럼 MakeArrayType을 호출할 때 차원 수를 지정하면 된다.
Type cubeType = typeof(int)MakeArrayType(3);  // 직육면체 형태
Console.WriteLine(cubeType == typeof(int[,,]));  // True
  • 반대로 배열 형식으로 그 원소 형식을 얻고 싶으면 GetElementType 메서드를 사용하면 된다.
Type e = typeof(int[]).GetElementType();  // e == typeof(int)
  • GetArrayRank 메서드는 직사각 배열의 차원 수를 돌려준다.
int rank = typeof(int[,,]).GetArrayRank();  // 3

중첩된 형식 얻기

  • 중첩된 형식(nested type; 내포된 형식)에 대한 Type 객체를 얻으려면 바깥쪽 형식(포함하는 형식)에 대해 GetNestedTypes를 호출한다. 예컨대 다음과 같다.
foreach (Type t in typeof(System.Environment).GetNestedTypes())
  Console.WriteLine(t.FullName);

// 출력
// System.EnvironmentSpecialFolder
  • 중첩된 형식을 다룰 때는 CLR이 중첩된 형식의 접근성을 특별하게 취급한다는 점을 주의해야 한다. CLR은 중첩된 형식이 중첩됨(nested)이라는 특별한 접근 수준을 가진다고 간주한다. 다음은 이를 보여주는 예이다.
Type t = typeof(System.Environment.SpecialFolder);
Console.WriteLine(t.IsPublic);  // False
Console.WriteLine(t.IsNestedPublic);  // True

형식 이름

  • 형식 객체에는 Namespace와 Name, FullName이라는 속성이 있다. 대부분의 경우 FullName은 그 앞의 둘을 합친 것이다.
Type t = typeof(System.Text.StringBuilder);

Console.WriteLine(t.Namespace);  // System.Text
Console.WriteLine(t.Name);  // StringBuilder
Console.WriteLine(t.FullName);  // System.Text.StringBuilder
  • 그러나 이 규칙에는 두 가지 예외가 있다. 바로 중첩된 형식과 닫힌 제네릭 형식이다.
  • Type에는 또한 AssemblyQualifiedName이라는 속성도 있다. 이 속성은 FullName 다음에 쉼표와 어셈블리의 완전 한정 이름이 오는 문자열을 돌려준다.
    • 이 문자열은 Type.GetType을 호출할 때 사용할 수 있는 이름과 같은 형태이며, 기본 적재 문맥에서 형식을 고유하게 식별한다.

중첩된 형식 이름

  • 중첩된 형식의 경우, 세 가지 이름 속성 중 중첩된 형식을 포함하는 형식의 이름이 있는 속성은 FullName 뿐이다.
Type t = typeof(System.Environment.SpecialFolder);
Console.WriteLine(t.Namespace);  // System
Console.WriteLine(t.Name);  // SpecialFolder
Console.WriteLine(tFull.Name);  // System.Environment+SpecialFolder
  • + 기호는 그 다음의 이름이 중첩된 이름공간이 아니라 이 형식을 포함하는 형식의 이름임을 말해준다.

제네릭 형식 이름

  • 제네릭 형식 이름에는 ‘ 기호와 형식 매개변수 개수로 이루어진 접미사가 붙는다. 특정 형식에 묶이지 않은(unbound; 형식 매개변수들의 바인딩이 완료되지 않은) 제네릭 형식의 경우에는 Name과 FullName 모두에 그러한 접미사가 붙는다.
Type t = typeof(Dictionary<,>);  //Unbound
Console.WriteLine(t.Name);  // Dictionary'2
Console.WriteLine(tFull.Name);  // System.Collections.Generic.Dictionary'2
  • 그러나 닫힌 제네릭 형식의 FullName에는(그리고 FullName에만) 각 형식 매개변수의 완전 한정 어셈블리 이름들로 이루어진 상당한 길이의 추가 문구가 붙는다.
Console.WriteLine(typeof(Dictionary<int, string>).FullName);

// 출력
// System.Collections.Generic.Dictionary'2[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5...], [System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5..]]
  • 이에 의해 AssemblyQualifiedName 속성(형식의 전체 이름과 어셈블리 이름의 조합)은 주어진 제네릭 형식과 그 형식 매개변수들을 완전히 식별하기에 충분한 정보를 담게 된다.

배열 형식과 포인터 형식 이름

  • 배열 형식의 이름에는 typeof 표현식에 사용한 것과 동일한 접미사가 붙는다.
Console.WriteLine(typeof(int[]).Name);  // Int32[]
Console.WriteLine(typeof(int[,]).Name);  // Int32[,]
Console.WriteLine(typeof(int[,]).FullName);  // System.Int32[,]
  • 포인터 형식도 마찬가지다.
Console.WriteLine(typeof(byte*).Name);  // Byte*

ref 및 out 매개변수 형식 이름

  • ref나 out 매개변수에 해당하는 형식의 이름에는 접미사 &가 붙는다.
Type t = typeof(bool).GetMethod("TryParse").GetParameters()[1].ParameterType;
Console.WriteLine(t.Name);  // Boolean&

기반 형식과 인터페이스

  • Type은 현재 형식의 기반 형식에 대한 형식 객체를 돌려주는 BaseType이라는 속성을 제공한다.
Type base1 = typeof(System.String).BaseType;
Type base2 = typeof(System.IO.FileStream).BaseType;

Console.WriteLine(base1.Name);  // Object
Console.WriteLine(base2.Name);  // Stream
  • GetInterfaces 메서드는 현재 형식이 구현하는 인터페이스에 대한 형식 객체를 돌려준다.
foreach (Type iType in typeof(Guid).GetInterfaces()0
  Console.WriteLine(iType.Name);

// 출력
// IFormattable
// IComparable
// IComparable'1
// IEquatable'1
  • Type은 C#의 정적 is 연산자의 동적 버전에 해당하는 두 메서드를 제공한다.
    • IsInstanceOfType
      • 주어진 인스턴스가 현재 형식의 한 인스턴스인지 판정한다.
    • IsAssignableFrom
      • 주어진 형식과 현재 형식의 호환 여부를 판정한다.
  • 다음은 IsInstanceOfType의 사용 예이다.
object obj = Guid.NewGuid();
Type target = typeof(IFormattable);

bool isTrue = obj is IFormattalbe;  // 정적 C# 연산자
bool alsoTrue = traget.IsInstanceOfType(obj);  // 해당 동적 버전
  • IsAssignableFrom은 좀 더 유연하다.
Type target = typeof(IComparable), source = typeof(string);
Console.WriteLine(tartget.IsAssignableFrom(source));  // True
  • IsSubclassOf 메서드는 IsAssignableFrom과 같되, 인터페이스를 배제한다.

형식의 인스턴스화

  • Type 객체를 이용해서 해당 형식의 인스턴스를 동적으로 생성하는 방법은 두 가지이다.
    • Type 객체를 인수로 해서 정적 Activator.CreateInstance 메서드를 호출한다.
    • Type 객체의 GetConstructor 메서드를 호출해서 얻은 ConstructorInfo 객체에 대해 Invoke 메서드를 호출한다.
  • Activator.CreateInstance는 Type 객체 하나와 생성자에 전달할 임의의 개수의 인수들(생략 가능)을 받는다.
int i = (int)Activator.CreateInstance(typeof(int));
DateTime dt = (DateTime)Activator.CreateInstance(typeof(DateTime), 2000, 1, 1);
  • CreateInstance 메서드에는 형식을 적재할 어셈블리나 대상 응용 프로그램 도메인, 비공용 생성자 바인딩 여부 등을 지정할 수 있는 다양한 중복적재 버전들이 있다.
    • 만일 런타임이 적절한 생성자를 찾지 못하면 MissingMethodException 예외가 발생한다.
  • 인수 값들만으로는 중복적재된 생성자들의 중의성을 해소할 수 없는 경우에는 둘째 방법, 즉 ConstructorInfo 객체에 대해 Invoke를 호출하는 방법이 필수이다.
    • 예컨대 X라는 클래스에 생성자가 두 개 있는데, 하나는 string 형식의 인수를 받고 다른 하나는 StringBuilder 형식의 인수를 받는다고 하자. 만일 Activator.CreateInstance에 추가 인수로 null을 지정하면 두 생성자 모두 유효하므로 주의성이 발생한다.
    • 이런 경우 다음처럼 ConstructorInfo를 사용해서 특정 생성자를 명시적으로 사용할 필요가 있다.
// string 형식의 매개변수 하나가 있는 생성자를 가져온다.
ConstructorInfo ci = typeof(X).GetConstructor(new[] { typeof(string) };

// 그 생성자에 null을 전달해서 객체를 생성한다.
object foo = ci.Invoke(new object[] { null });
  • 비공용 생성자에 접근하려면 적절한 BindingFlags 인수를 지정해야 하는데, 이에 관해서는 ‘비공용 멤버 접근’을 보기 바란다.
  • 동적 인스턴스화로 객체를 생성하면 정적 인스턴스화보다 몇 마이크로초가 더 걸린다. 보통의 경우 CLR이 객체의 인스턴스화를 아주 빠르게 수행한다는 점에서 (작은 클래스에 대한 간단한 new는 수십 분의 1나노초 수준이다), 이는 상대적으로 꽤 긴 시간이다.
  • 요소 형식으로 배열을 동적으로 인스턴스화하려면 먼저 MakeArrayType을 호출해서 배열 형식을 얻은 후 앞에서처럼 진행하면 된다.
    • 제네릭 형식의 동적 인스턴스화도 가능한데, 다음 절에서 설명하겠다.
  • 대리자를 동적으로 인스턴스화 할 때는 Delegate.CreateDelegate를 호출한다. 다음 예에서 보듯이, 인스턴스 대리자 뿐만 아니라 정적 대리자도 동적으로 인스턴스화 할 수 있다.
class Program
{
  delegate int IntFunc(int x);
  static int Square (int x) { return x * x; }  // 정적 메서드
  int Cube (int x) { return x * x * x; }  // 인스턴스 메서드

  static void Main()
  {
    Delegate staticD = Delegate.CreateDelegate(typeof(IntFunc), typeof(Program), "Square");
    Delegate instanceD = Delegate.CreateDelegate(typeof(IntFunc), new Program(), "Cube");

    Console.WriteLine(staticD.DynamicInvoke(3));  // 9
    Console.WriteLine(instanceD.DynamicInvoke(3));  // 37
  }
}
  • 이 예제처럼 반환된 Delegate 객체의 DynamicInvoke를 호출하는 대신, 다음처럼 그 객체를 적절한 대리자 형식으로 캐스팅해서 호출할 수도 있다.
IntFunc f = (IntFunc) staticD;
Console.WriteLine(f(3));  // 9 (이 방법이 훨씬 빠르다!)
  • 메서드 이름 대신 MethodInfo 객체로 CreateDelegate를 호출할 수 있다. 이에 대한 설명은 ‘멤버의 반영과 호출’에서 하겠다.

제네릭 형식의 인스턴스화

  • Type은 닫힌 제네릭 형식과 묶이지 않은 제네릭 형식도 대표한다. 컴파일 시점에서처럼 닫힌 제네릭 형식은 인스턴스화할 수 있지만, 묶이지 않은 제네릭 형식은 그렇지 않다.
Type closed = typeof(List<int>);
List<int> list = (List<int>)Activator.CreteInstance(closed);  // OK

Type unbound = typeof(List<>);
object anError = Activator.CreateInstance(unbound);  // 실행시점 오류
  • 묶이지 않은 제네릭 형식을 닫힌 제네릭 형식으로 변환하려면 MakeGenericType 메서드를 사용한다. 원하는 형식 인수들을 지정해서 호출하면 된다.
Type unbound = typeof(List<>);
Type closed = unbound.MakeGenericType(typeof(int));
  • GetGenericTypeDefinition 메서드는 그 반대의 일을 한다.
Type unbound2 = closed.GetGenericTypeDefinition();  // unbound == unbound2
  • IsGenericType 속성은 만일 Type 객체가 제네릭 형식을 대표하면 true르 ㄹ돌려준다. 그런 경우 IsGenericTypeDefinition 속성은 만일 그 제네릭 형식이 묶이지 않은 제네릭이면 true를 돌려준다.
    • 다음은 주어진 형식이 널 가능 값 형식인지 판정하는 방법을 보여주는 예이다.
Type nullable = typeof(bool?);
Console.WriteLine(nullable.IsGenericType && nullable.GetGenericTypeDefinition() == typeof(Nullable<>));  // True
  • GetGenericArguments 메서드는 닫힌 제네릭 형식의 형식 인수들을 돌려준다.
Console.WriteLine(closed.GetGenericArguments()[0]);  // System.Int32
Console.WriteLine(nullable.GetGenericArguments()[0]);  // System.Boolean
  • 묶이지 않은 제네릭 형식의 경우 GetGenericArguments는 제네릭 형식의 정의에 지정된 자리표 형식들에 해당하는 유사 형식 객체들을 돌려준다.
Console.WriteLine(unbound.GetGenericArguments()[0]);  // T
  • 실행시점에서 모든 제네릭 형식은 묶이지 않은 제네릭 아니면 닫히 ㄴ제네릭이다. 묶이지 않은 제네릭은 typeof(Foo<>) 같은 (비교적 드문) 표현식에 해당하며, 그 외의 제네릭 형식은 닫힌 제네릭이다.
    • 실행시점에서 열린 제네릭 형식 같은 것은 없다. 모든 열린 형식은 컴파일러가 닫기 때문이다. 다음 클래스의 메서드는 항상 False를 출력한다.
class Foo<T>
{
  public void Test()
  {
    Console.Write(GetType().IsGenericTypeDefinition);
  }
}

멤버의 반영과 호출

  • GetMembers 메서드는 현재 형식의 멤버들을 돌려준다. 다음과 같은 클래스가 있다고 하자.
class Walnut
{
  private bool cracked;
  public void Crack() { cracked = true; }
}
  • 다음은 이 클래스의 공용 멤버들을 파악하는 예이다.
MemberInfo[] members = typeof(Walnut).GetMembers();
foreach (MemberInfo m in members)
  Console.WriteLine(m);

// 결과
// Void Crack()
// System.Type GetType()
// System.String ToString()
// Boolean Equals(System.Object)
// Int32 GetHashCode()
//Void .ctor()
  • 아무 인수 없이 GetMembers를 호출하면 현재 형식(그리고 그 기반 형식들의)의 모든 공용 멤버가 반환된다. GetMember는 주어진 이름에 해당하는 특정한 하나의 멤버를 조회하는데, 멤버가 중복적재되었을 수도 있기 때문에 반환값은 배열이다.
MemberInfo[] m = typeof(Walnut).GetMembers("Crack");
Console.WriteLine(m[0]);  // Void Crack()
  • MemberInfo에는 MemberType이라는 속성도 있다. 이 속성의 형식은 다음과 같은 값들이 정의된 플래그 열거형 MemberTypes이다.
All/ Custom/ Field/ NestedType/ TypeInfo/ Constructor/ Event/ Method/ Property
  • GetMembers 호출 시 특정 MemberTypes 값을 지정해서 그에 해당하는 종류의 멤버들만 얻을 수도 있다. 아니면 GetMethods나 GetFields, GetProperties, GetEvents, GetConstructors, GetNestedTypes 같은 구체적인 메서드를 호출해서 해당 종류의 멤버들만 얻는 것도 가능하다.
    • 이 메서드들에는 특정한 하나의 멤버만 조회하는 메서드 이름이 단수인 버전도 존재한다.
  • 형식의 멤버를 조회할 때는 최대한 구체적으로 멤버를 지정하는 것이 도움이 된다. 그렇게 하면 나중에 새 멤버가 추가되어도 코드가 망가지지 않기 때문이다.
    • 예컨대 이름으로 메서드 하나를 조회할 때 모든 매개변수 형식을 지정하면 나중에 그 메서드가 중복적재되어도 코드가 여전히 잘 작동한다.
  • MemberInfo 객체에는 멤버 이름을 돌려주는 Name 속성과 Type 형식의 객체를 돌려주는 다음의 두 속성이 있다.
    • DeclaringType
      • 현재 멤버를 정의하는 형식에 대한 Type 객체를 돌려준다.
    • ReflectedType
      • GetMembers를 호출한 객체의 형식에 대한 Type 객체를 돌려준다.
  • 기반 형식에 정의되어 있는 멤버에 대한 MemberInfo 객체의 경우 이 두 속성이 다르다. 그런 경우 DeclaringType은 기반 형식에 해당하는 Type 객체를 돌려주지만, ReflectedType은 파생 형식에 해당하는 Type 객체를 돌려준다.
    • 다음은 이 점을 보여주는 예이다.
class Program
{
  static void Main()
  {
    MethodInfo test = typeof(Program).GetMethod("ToString");
    MethodInfo obj = typeof(object).GetMethod("ToString");

    Console.WriteLine(test.DeclaringType);  // System.Object
    Console.WriteLine(obj.DeclaringType);  // System.Object

    Console.WriteLine(test.ReflectedType);  // Program
    Console.WriteLine(obj.ReflectedType);  // System.Object

    Console.WriteLine(test == obj);  // False
  }
}
  • test 객체와 obj 객체는 ReflectedType이 다르므로 상등이 아니다. 그러나 그러한 차이는 반영 API 수준에서만 의미가 있다. 바탕 형식 체계에서 Program 클래스의 ToString 메서드 자체는 여전히 하나뿐이다. 두 MethodInfo 객체가 사실은 같은 ToString 메서드를 지칭한다는 점을 다음 두 가지 방식으로 확인할 수 있다.
Console.WriteLine(test.MethodHandle == obj.MethodHandle);  // True
Console.WriteLine(test.MetadataToken == obj.MetadataToken && test.Module == obj.Module);  // True
  • MethodHandle 속성은 한 응용 프로그램 도메인 안의 모든 메서드에 대해 고유하고, MetadataToken 속성은 한 어셈블리 모듈 안의 모든 형식과 멤버에 대해 고유하다.
  • MemberInfo에는 또한 커스텀 특성을 돌려주는 메서드들도 정의되어 있다.
  • 현재 실행 중인 메서드의 MethodBase 객체는 MethodBase.GetCurrentMethod 메서드로 얻을 수 있다.

멤버 정보 형식

  • MemberInfo 자체는 하나의 추상 기반 클래스일 뿐이다. 구체적인 멤버의 정보를 실제로 제공하는 것은 아래 그림에 나온 이 클래스의 파싱 클래스들이다.

  • GetMembers 등으로 얻은 MemberInfo 객체는 그 MemberType 속성에 맞게 구체적인 파생 형식으로 캐스팅해서 사용할 수 있다.
    • 단 GetMethod나 GetField, GetProperty, GetEvent, GetConstructor, GetNestedType(그리고 이름이 복수인 버전들)으로 얻은 객체는 캐스팅할 필요가 없다. 아래 표는 C#의 각 언어 요소에 해당하는 메서드를 정리한 것이다.
C# 요소 사용할 메서드 인수 결과
메서드 GetMethod (메서드 이름) MethodInfo
속성 GetProperty (속성 이름) PropertyInfo
인덱서 GetDefaultMembers MemberInfo[] (C#으로 컴파일된 경우에는 PropertyInfo 객체들을 담고 있음)
필드 GetField (필드 이름) FieldInfo
열거형 멤버 GetField (멤버 이름) FieldInfo
이벤트 GetEvent (이벤트 이름) EventInfo
생성자 GetConstructor ConstructorInfo
종료자 GetMethod “Finalize” MethodInfo
연산자 GetMethod “op_” + 연산자 이름 MethodInfo
중첩된 형식 GetNestedType (형식 이름) Type

 

  • MemberInfo의 각 파생 클래스에는 다양한 속성과 메서드가 있으며, 이들을 통해서 멤버의 메타자료의 모든 측면에 접근할 수 있다. 이를테면 멤버의 가시성, 수정자, 매개변수, 제네릭 형식 인수, 반환 형식, 커스텀 특성들을 알아낼 수 있다.
  • 다음은 GetMethod 메서드를 사용하는 예제이다.
MethodInfo m = typeof(Walnut).GetMethod("Crack");
Console.WriteLine(m);  // Void Crack()
Console.WriteLine(m.ReturnType);  // System.Void
  • 반영 API는 처음 사용되는 모든 *Info 인스턴스를 캐시에 저장한다.
MethodInfo method = typeof(Walnut).GetMethod("Crack");
MemberInfo member = typeof(Walnut).GetMember("Crack")[0];
Console.Write(method == member);  // True
  • 이러한 캐시는 객체의 신원 유지는 물론 성능에도 도움이 된다. 만일 캐시가 없었다면 반영 API는 상당히 느린 API가 되었을 것이다.

C# 멤버 대 CLR 멤버

  • C#의 기능 요소들과 CLR의 요소들이 일대일로 대응되지는 않는다. CLR과 반영 API가 C#만이 아니라 모든 .NET 언어를 염두에 두고 설계된 것이라는 점을 생각하면 당연하다. 심지어 Visual Basic에서도 반영 기능을 사용할 수 있다.
  • CLR의 관점에서 C#의 인덱서, 열거형, 연산자, 종료자는 다른 무언가로 번역되는 2차적인 존재이다. 좀 더 구체적으로 말하면 다음과 같다.
    • C# 인덱서는 하나 이상의 인수들을 받는, 그리고 해당 형식에서 [DefaultMember] 특성이 부여된 하나의 속성으로 번역된다.
    • C# 열거형은 열거형 멤버마다 정적 필드가 있는 System.Enum 파생 형식으로 변역된다.
    • C# 연산자는 “op_”로 시작하는 특별한 이름을 가진 정적 메서드(이를테면 “op_Addition”)로 번역된다.
    • C# 종료자는 Object의 Finalize를 재정의하는 메서드로 정의된다.
  • 게다가 속성과 이벤트 자체도 다음 두 요소로 구성되는 복합적인 존재라서 사정이 더욱 복잡해 진다.
    • 속성 또는 이벤트를 서술하는 메타자료(PropertyInfo 또는 EventInfo로 캡슐화된)
    • 하나 또는 두 개의 바탕 메서드
  • C# 프로그램에서 바탕 메서드들은 속성 또는 이벤트의 정의 자체에 포함된다. 그러나 이를 IL로 컴파일하면 그 메서드들은 다른 메서드와 마찬가지 방식으로 호출할 수 있는 보통의 메서드로 변한다.
    • 이 때문에 GetMethods는 보통의 메서드 뿐만 아니라 속성이나 이벤트의 바탕 메서드들도 돌려준다.
    • 다음은 이점을 보여주는 예이다.
class Test { public int X { get { return 0; } set { } } }

void Demo()
{
  foreach (MethodInfo mi in typeof (Test).GetMethods())
    Console.Write(mi.Name + " ");
}

// 출력
// get_X Set_X GetType ToString Equals GetHashCode
  • 특별한 바탕 메서드들은 MethodInfo의 IsSpecialName 속성으로 식별할 수 있다. 만일 IsSpecialName 속성이 true이면 그 메서드는 속성이나 인덱서, 이벤트, 연산자를 위한 메서드인 것이다. IsSpecialName 속성은 보통의 C# 메서드와 Finalize 메서드(종료자가 정의되어 있는 경우)에 대해서만 false를 돌려준다.
  • 다음은 C# 컴파일러가 생성하는 바탕 메서드들이다.
C# 언어 요소 멤버 형식 IL의 메서드
속성 Property get_XXX와 Set_XXX
인덱서 Property get_Item과 set_Item
이벤트 Event add_XXX와 remove_XXX

 

  • 각 바탕 메서드에는 그와 연관된 MethodInfo 객체가 있다. 다음은 그 객체들에 접근하는 방법을 보여주는 예이다.
PropertyInfo pi = tyoeof(Console).GetProperty("Title");
MethodInfo getter = pi.GetGetMethod();  // get_Title
MethodInfo setter = pi.GetSetMethod();  // set_Title
MethodInfo[] both = pi.GetAccessors();  // Length==2
  • 이벤트의 경우에는 EventInfo의 GetAddMethod, GetRemoveMethod 메서드를 사용하면 된다.
  • 반대 방향으로 가려면, 즉 MethodInfo 객체에서 해당 PropertyInfo나 EventInfo 객체를 얻으려면 질의를 수행해야 한다. 그런 용도로 LINQ가 이상적이다.
PropertyInfo p = mi.DeclaringType.GetProperties().First(x => x.GetAccessors(true).Contains(mi));

제네릭 형식 멤버

  • 닫힌 제네릭 형식뿐만 아니라 묶이지 않은 제네릭 형식의 멤버에 대한 메타자료도 얻을 수 있다.
PropertyInfo unbound = typeof(IEnumerrator<>).GetProperty("Current");
PropertyInfo closed = typeof(IEnumerrator<int>).GetProperty("Current");

Console.WriteLine(unbound);  // Int32 Count
Console.WriteLine(closed);  // Int32 Count

Console.WriteLine(unbound == closed);  // False
Console.WriteLine(unbound.DeclaringType.IsGenericTypeDefinition);  // True
Console.WriteLine(closed.DeclaringType.IsGenericTypeDefinition);  // False
  • 묶이지 않은 제네릭 형식의 멤버는 동적으로 호출할 수 없다.

멤버의 동적 호출

  • MethodInfo나 PropertyInfo, FieldInfo 객체를 얻었다면 동적으로 해당 멤버를 호출하거나 그 값을 설정/조회 할 수 있다.
    • 어떤 멤버를 호출할 것인지를 컴파일 시점이 아니라 실행시점에서 선택한다는 점에서 이를 동적 바인딩(dynamic binding) 또는 늦은 바인딩(late binding)이라고 부른다.
  • 이해를 돕는 예로 다음은 보통의 정적 바인딩(static binding)에 해당한다.
string s = "Hello";
int length = s.Length;
  • 다음은 같은 일을 반영 기능을 이용해서 동적으로 수행하는 예이다.
object s = "Hello";
PropertyInfo prop = s.GetType().GetProperty("Length");
int length = (int)prop.GetValue(s, null);  // 5
  • GetValue와 SetValue는 PropertyInfo나 FieldInfo의 값을 조회 또는 설정한다.
    • 첫 인수는 인스턴스인데, 정적 멤버의 경우에는 null을 지정하면된다. 인덱서에 접근하는 것은 “Item” 이라는 이름의 속성에 접근하는 것과 같다.
    • 단, GetValue나 SetValue를 호출할 때 둘째 인수로 인덱서 값을 제공해야 한다는 점이 다르다.
  • 메서드를 동적으로 호출할 때는 해당 MethodInfo 객체에 대해 Invoke 메서드를 호출한다.
    • 이때 메서드에 전달할 인수들의 배열을 지정한다. 그 인수들 중에 형식이 잘못된 것이 하나라도 있으면 CLR이 예외를 던진다.
    • 동적 호출에는 컴파일 시점 형식 안전성이 적용되지 않지만, 실행시점 형식 안전성은 여전히 적용된다(dynamic 키워드에서와 마찬가지이다)

메서드 매개변수

  • string의 Substring 메서드를 동적으로 호출한다고 하자. 먼저 정적으로 호출할 떄는 그냥 다음과 같이 하면 된다.
Console.WriteLine("stamp".Substring(2));  // "amp"
  • 다음은 이를 반영 기능을 이용해서 동적으로 수행하는 예이다.
Type type = typeof(string);
Type[] parameterTypes = { typeof(int) };
MethodInfo method = type.GetMethod("Substring", parameterTypes);

object[] arguments = { 2 };
object returnValue = method.Invoke("stamp", arguments);
Console.WriteLine(returnValue);  // "amp"
  • Substring은 중복적재되어 있기 때문에, 이 예제에서 GetMethod 호출 시 매개변수 형식들의 배열을 지정해서 특정 중복적재 버전을 선택해야 했다.
    • 매개변수 형식들을 지정하지 않으면 GetMethod가 AmbigousMatchException 예외를 던질 수 있다.
  • MethodBase(MethodInfo와 ConstructorInfo의 기반 클래스)에 정의된 GetParameters 메서드는 매개변수 메타자료를 돌려준다.
    • 앞의 예제에 이어서 다음은 Substring의 매개변수들에 대한 정보를 얻는 예이다.
ParameterInfo[] paramList = method.GetParameters();
foreach (ParameterInfo x in paramList)
{
  Console.WriteLine(x.Name);  // startIndex
  Console.WriteLine(x.ParameterType);  // System.Int32
}

ref 및 out 매개변수 다루기

  • 메서드를 동적으로 호출할 떄 ref나 out 매개변수를 넘겨 주려면 메서드에 대한 메타자료를 얻기 전에 해당 매개변수 형식에 대해 MakeByRefType을 호출하는 과정이 필요하다.
    • 예컨대 다음과 같은 정적 호출 코드가 있다고 할 때
int x;
bool successfulParse = int.TryParse("23", out x);
  • 이를 동적으로 수행하려면 다음과 같이 하면 된다.
object[] args = { "23", 0 };
Type[] argTypes = { typeof(string), typeof(int).MakeByRefType() };
MethodInfo tryParse = typeof(int).GetMethod("TryParse", argTypes);
bool successfulParse = (bool)tryParse.Invoke(null, args);

Console.WriteLine(successfulParse + " " + args[1]);  // True 23
  • 이 예제는 out 매개변수에 관한 것이었지만, ref 매개변수도 마찬가지로 처리하면 된다.

제네릭 메서드의 조회와 호출

  • 중복 적재된 메서드의 중의성을 해소하려면 GetMethod 호출 시 매개변수들 형식들을 명시적으로 지정해야 할 필요가 있다. 그런데 제네릭 매개변수 형식들은 그런 식으로 지정할 수 없다.
    • 예컨대 System.Linq.Enumerable 클래스의 Where 메서드를 생각해 보자. 이 메서드는 다음과 같이 중복적재되어 있다.
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, int, bool> predicate);
  • 특정한 하나의 중복적재 버전에 접근하려면 모든 버전을 조회해서 그중 하나를 직접 찾아내야 한다. 다음은 첫 버전을 조회하는 질의이다.
from m in typeof(Enumerable).GetMethods()
where m.Name == "Where" && m.IsGenericMethod
let parameters = m.GetParameters()
where parameters.Length == 2
let getArg = m.GetGenericArguments().First()
let enumerableOfT = typeof(IEnumerable<>).MakeGenericType(genArg)
let funcOfTBool = typeof(Func<,>).MakeGenericType(getArg, typeof(bool))
where parameters[0].ParameterType == enumerableOfT && parameters[1].ParameterType == funcOfTBool
select m
  • 이 질의에 대해 .Single()을 호출하면 묶이지 않은 형식 매개변수들이 있는 정확한 MethodInfo 객체가 반환된다. 다음으로 할 일은 MakeGenericMethod를 호출해서 형식 매개변수들을 닫는 것이다.
var closedMethod = unboundMethod.MakeGenericMethod(typeof(int));
  • 이 경우에는 TSource에 int를 지정해서 메서드를 닫았다. 이제 source 매개변수가 IEnumerable<int> 형식이고 predicate 매개변수가 Func<int, bool> 형식인 Enumerable.Where를 호출할 수 있다. 우선 해당 인수들을 준비한다.
int[] source = { 3, 4, 5, 6, 7, 8 };
Func<int, bool> predicate = n => n % 2 == 1; // 홀수들만 선택한다.
  • 마지막으로 닫힌 제네릭 메서드를 다음과 같이 호출한다.
var query = (IEnumerable<int>) closedMethod.Invoke(null, new object[] { source, predicate });

foreach (int element in query)
  Console.Write(element + "|");  // 3|5|7
  • System.Linq.Expressions API를 이용해서 표현식을 동적으로 구축한다면 제네릭 메서드를 이처럼 번거로운 방식으로 지정할 필요가 없다. 호출하고자 하는 메서드의 닫힌 제네릭 형식 인수들을 받는 Expression.Call 메서드 중복적재 버전을 사용하면 그만이다.
int[] source = { 3, 4, 5, 6, 7, 8 };
Func<int, bool> predicate = n => n % 2 == 1;

var sourceExpr = Expression.Constant(source);
var predicateExpr = Expression.Constant(predicate);

var callExpression = Expression.Call(
  typeof(Enumerable), 
  "Where", 
  new[] { typeof(int) }, // 닫힌 제네릭 매개변수 형식
  sourceExpr,
  predicateExpr);

대리자를 이용한 성능 향상

  • 동적 호출은 상대적으로 비효율적이다. 호출당 추가부담은 보통 몇 마이크로초 정도이다. 루프에서 메서드를 여러 번 호출하는 경우, 원하는 동적 메서드를 가리키는 대리자를 동적으로 인스턴스화 해서 사용함으로써 호출당 추가부담을 몇나노초 수준으로 줄일 수 있다.
    • 다음은 string의 Trim 메서드를 큰 추가부담 없이 동적으로 백만 번 호출하는 예이다.
delegate string StringToString (string s);

static void Main()
{
  MethodInfo trimMethod = typeof(string).GetMethod("Trim", new Type[0]);
  var trim = (StringToString) Delegate.CreateDelegate(typeof(StringToString), trimMethod);

  for (int i = 0; i < 1000000; i++)
    trim("test");
}
  • 이 방법이 빠른 것은 동적 바인딩이 루프 밖에서 단 한번만 일어나기 때문이다.

비공용 멤버 접근

  • 형식의 메타자료에 접근하는데 쓰이는 모든 메서드(GetProperty, GetField 등)에는 BindingFlags 열거형을 받도록 중복적재된 버전이 존재한다.
    • 이 열거형은 기본적인 검색 조건을 변경하는 일종의 메타자료 필터로 쓰인다. 가장 흔한 용도는 비공용(nonpublic) 멤버들만 조회하는 것이다(데스크톱 응용 프로그램에서만 가능하다)
  • 한 예로 다음과 같은 클래스를 생각해 보자.
class Walnut
{
  private bool cracked;
  public void Crack() { cracked = true; }

  public override string ToString() { return cracked.ToString(); }
}
  • 다음은 호두를 까지 않은 상태로 되돌리는 예이다.
Type t = typeof(Walnut);
Walnut w = new Walnut();
w.Crack();
FieldInfo f = t.GetField("cracked", BindingFlags.NonPublic | BindingFlags.Instance);
f.SetValue(w, false);
Console.WriteLine(w);  // False
  • 반영을 이용해서 비공용 멤버에 접근하는 것은 강력하지만 위험한 일임을 기억하기 바란다. 이런 접근은 캡슐화를 우회하므로, 한 형식의 내부 구현에 대한 의존성이 감당할 수 없을 정도로 큰 코드가 만들어질 수 있다.

BindingFlags 열거형

  • BindingFlags의 멤버들은 비트별 연산으로 조합해서 쓰도록 설계되었다. 어떤 조합을 원하든 일단은 다음과 같은 기본적인 조합 중 하나로 시작해야 한다.
BindingFlags.Public | BindingFlags.Instance
BindingFlags.Public | BindingFlags.Static
BindingFlags.NonPublic | BindingFlags.Instance
BindingFlags.NonPublic | BindingFlags.Static
  • NonPublic은 internal, protected, protected internal, priavate를 포함한다.
  • 다음 예제는 object 형식의 모든 공용 정적 멤버를 조회한다.
BindingFlags publicStatic = BindingFlags.Public | BindingFlags.Static;
MemberInfo[] members = typeof(object).GetMembers(publicStatic);
  • 다음은 object 형식의 모든 비공용 정적 멤버와 비공용 인스턴스를 조회하는 예이다.
BindingFlags nonPublicBinding = BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
MemberInfo[] members = typeof(object).GetMembers(nonPublicBinding);
  • DeclaredOnly 플래그는 기반 형식에서 상속된 메서드들을 제외한다. 단, 현재 형식이 재정의한 메서드들은 포함된다.
    • DeclaredOnly 플래그는 다소 혼도으이 여지가 있다. 결과 집합을 확장하는 BindingFlags의 다른 플래그들과는 달리 이 플래그는 결과 집합을 제한한다.

제네릭 메서드

  • 제네릭 메서드는 직접 호출할 수 없다. 다음 예제 코드는 예외를 던진다.
class Program
{
  publid static T Echo<T> (T x) { return x; }

  static void Main()
  {
    MethodInfo echo = typeof(Program).GetMethod("Echo");
    Console.WriteLine(echo.IsGenericMethodDefinition);  // True
    echo.Invoke(null, new object[] { 123 });  // 예외
  }
}
  • 제네릭 메서드를 호출하려면 추가적인 단계가 필요하다. 바로 MethodInfo에 대해 MakeGenericMethod 메서드를 호출해서 구체적인 제네릭 형식 매개변수들을 지정하는 것이다. 그 메서드가 돌려준 MethodInfo를 이용해서 제네릭 메서드를 호출하면 된다. 다음이 그러한 예이다.
MethodInfo echo = typeof(Program).GetMethod("Echo");
MethodInfo intEcho = echo.MakeGenericMethod(typeof(int));
Console.WriteLine(intEcho.IsGenericMethodDefinition);  // False
Console.WriteLine(intEcho.Invoke(null, new object[] { 3 });  // 3

제네릭 인터페이스 멤버의 익명 호출

  • 반영 기능은 어떤 제네릭 인터페이스의 한 메서드를 호출하고자 할 때, 그러나 그 메서드의 형식 매개변수들을 실행시점에서야 결정할 수 있을 때 유용하다.
    • 이론적으로 형식들을 완벽하게 설계해다면 그런 동적 호출이 필요한 상황이 거의 발생하지 않는다. 그러나 다들 알다시피 우리가 형식들을 항상 완벽하게 설계하지는 않는다.
  • 한 예로 LINQ 질의의 결과를 확장할 수 있는 좀 더 강력한 버전의 ToString을 작성한다고 하자. 우선 다음과 같은 틀로 시작할 수 있을 것이다.
public static string ToStringEx<T>(IEnumerable<T> sequence)
{
  ...
}
  • 그러나 첫 틀부터 상당히 제한적이다. 만일 sequence에 중첩된 컬렉션이 들어 있고 그 컬렉션들도 확장하고 싶다면 어떻게 해야 할까? 이를 위해서는 다음과 같은 중복적재 버전을 추가해야 할 것이다.
public static string ToStringEx<T>(IEnumerable<IEnumerable<T>> sequence)
  • 그런데 sequence에 중첩된 순차열들의 그루핑, 즉 투영이 들어 있으면 어떻게 할까? 이런 요구가 생길 떄마다 메서드를 또다시 중복적재하는 정적인 해법은 비현실적이다.
    • 그보다는 임의의 객체 그래프를 처리할 수 있을 정도로 확장성이 좋은 접근방식이 바람직하다. 다음으 그러한 접근방식의 하나이다.
public static string ToStringEx<T>(IEnumerable<T> sequence)
{
  if (value == null) return "<null>";
  StringBuilder sb = new StringBuilder();

  if (value is List<>)  // 오류
    sb.Append("List of " + ((List<>)value).Count + " items");  // 오류

  if (value is IGrouping<,>)  // 오류
    sb.Append("Group with key=" + ((IGrouping<,>)value).Key);  // 오류

  // 만일 이것이 컬렉션이면, ToStringEx()를 재귀적으로 호출해서 컬렉션 요소들을 열거한다.

  return sb.ToString();
}
  • 안타깝게도 이 코드는 컴파일되지 않는다. List<>나 IGrouping<>처럼 묶이지 않은 제네릭 형식의 멤버는 호출할 수 없다. List<>라면 대신 비제네릭 IList 인터페이스를 이용해서 문제를 해결할 수 있다.
if (value is IList)
  sb.Append("A list of " + ((IList)value).Count + " items");
  • 이것이 가능한 이유는 List<> 설계자들이 이런 상황을 예견하고 비제네릭 IList를 (그리고 제네릭 IList도) 구현했기 때문이다.
    • 독자가 제네릭 형식을 작성할 때에도 이러한 원리를 적용해 보기 바란다. 소비자가 최후의 수단으로 사용할 수 있도록 비제네릭 인터페이스나 비제네릭 기반 클래스를 두는 것은 대단히 가치 있는 일이다.
  • 그러나 IGrouping<,> 인터페이스에는 이런 간단한 해결책을 적용할 수 없다. 이 인터페이스는 다음과 같이 정의되어 있다.
public interface IGrouping<TKey, TElement> : IEnumerable<TElement>, IEnumerable
{
  TKey Key { get; }
}
  • Key 속성에 접근하는데 사용할 비제네릭 형식이 없음을 주목하기 바란다. 따라서 반영 기능을 사용할 수 밖에 없다.
    • 해결책은 묶이지 않은 제네릭 형식의 멤버를 그대로 호출하려 들지 말고(그런 호출은 불가능하다) 실행시점에서 먼저 형식 인수들을 지정해서 닫힌 제네릭 형식을 만든 후에 멤버를 호출하는 것이다.
  • 처음으로 할 일은 value가 IGrouping<,>를 구현하는지 판정해서(가장 쉬운 방법은 LINQ 질의를 이용하는 것이다) 만일 구현한다면 닫힌 제네릭 인터페이스를 얻는 것이다. 그런 다음에는 Key 속성을 조회해서 호출하면 된다.
public static string ToStringEx<T>(IEnumerable<T> sequence)
{
  if (value == null) return "<null>";
  if (value.GetType().IsPrimitive) return value.ToString();

  StringBuilder sb = new StringBuilder();

  if (value is ILis)
    sb.Append("항목 " + ((IList)value).Count + "개로 이루어진 목록:");

  Type closedIGrouping = value.GetType().GetInterfaces().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IGrouping<,>)).FirstOrDefault();
  
  if (closedIGrouping != null)  // IGrouping<,>에 대해 Key를 호출
  {
    PropertyInfo pi = closedIGrouping.GetProperty("Key");
    object key = pi.GetValue(value, null);
    sb.Append("키가 " + key + "인 그룹: ");
  }

  if (value is IEnumerable)
    foreach (object element in ((IEnumerable)value))
      sb.Append(ToStringEx(element) + " ");

  if (sb.Length == 0) sb.Append(value.ToString());

  return "\r\n" +  sb.ToString();
}
  • 이 접근방식은 안정적이다. IGrouping<,>가 암묵적으로 구현디었든 명시적으로 구현되었든 잘 작동한다. 다음은 이 ToStringEx의 사용 예이다.
Console.WriteLine(ToStringEx(new List<int> { 5, 6, 7 } ));
Console.WriteLine(ToStringEx("xyyzzz"GroupBy(c => c) ));

// 출력
// 항목 3개로 이루어진 목록: 5 6 7
// 키가 x인 그룹: x
// 키가 y인 그룹: y y
// 키가 z인 그룹: z z z

어셈블리의 반영

  • 반영 기능을 이용해서 어셈블리의 메타자료에 접근하는 것도 가능하다. Assembly 객체에 대해 GetType이나 GetTypes를 호출하면 된다.
    • 다음은 현재 어셈블리의 Demos 이름공간에 있는 TestProgram이라는 형식에 접근하는 예이다.
Type t = Assembly.GetExecutingAssembly().GetType("Demos.TestProgram");
  • 다음 예제는 e:\demo에 있는 mylib.dll이라는 어셈블리의 모든 형식을 나열한다.
Assembly a = Assembly.LoadFrom(@"e:\demo\mylib.dll");

foreach (Type t in a.GetTypes())
  Console.WriteLine(t);
  • Windows 스토어 앱에서는 다음과 같이 하면 된다.
Assembly a = typeof(Foo).GetTypeInfo().Assembly;

foreach (Type t in a.ExportedTypes)
  Console.WriteLine(t);
  • GetTypes와 ExportedTypes는 최상위 형식들만 돌려줄 뿐 중첩된 형식들은 돌려주지 않는다.

어셈블리를 반영 전용 문맥에 적재

  • 앞의 예제는 어셈블리의 형식들을 나열하기 위해 어셈블리를 현재 응용 프로그램의 도메인에 실제로 적재했다. 그런데 그렇게 하면 정적 생성자들이 실행되거나 이후의 형식 환원이 엉망이 되는 등의 부작용이 발생할 수 있다.
    • 형식 정보를 조사만 하려는(형식을 실제로 인스턴스화하거나 호출하는 것이 아니라) 경우, 해결책은 다음처럼 어셈블리를 반영 전용(reflection-only) 문맥에 적재하는 것이다(데스크톱에서만 가능)
Assembly a = Assembly.ReflectionOnlyLoadFrom(@"e:\demo\mylib.dll");
Cosole.WriteLine(a.ReflectionOnly);  // True

foreach (Type t in a.GetTypes())
  Console.WriteLine(t);
  • 이를 출발점으로 삼아서, 이를테면 클래스 탐색기를 만들 수 있다.
  • 어셈블리를 반영 전용 문맥으로 적재하는 메서드는 다음 3가지 이다.
    • ReflectionOnlyLoad(byte[])
    • ReflectionOnlyLoad(string)
    • ReflectionOnlyLoadFrom(string)
  • 반영 전용 문맥에서도 mscorlib.dll의 여러 버전을 적재하는 것은 불가능하다. 그런 일을 하려면 Microsoft의 CCI 라이브러리나 Mono.Cecil 같은 추가적인 라이브러리를 사용해야 한다.

모듈

  • 다중 모듈 어셈블리에 대해 GetTypes를 호출하면 모든 모듈의 모든 형식이 반환된다. 따라서 모듈의 존재를 무시하고 그냥 어셈블리 자체를 형식들의 컨테이너로 간주해도 된다. 그러나 모듈의 존재가 중요한 경우가 하나 있다. 바로 메타자료 토큰을 다룰 때이다.
  • 메타자료 토큰(metadata token)은 한 모듈의 범위 안에서 형식, 멤버, 문자열, 자원을 고유하게 식별하는 정수이다. IL은 메타자료 토큰을 사용하므로, IL 코드를 파싱하려면 그 토큰을 실제 구성요소로 환원할 수 있어야 한다.
    • 이를 위한 메서드들은 Module 형식에 정의된 ResolveType, ResolveMember, ResolveString, ResolveSignature이다.
  • 어셈블리의 모든 모듈의 목록을 얻으려면 GetModules 메서드를 호출한다.
    • 어셈블리의 주 모듈에 직접 접근하는 것도 가능하다. ManifestModule 속성을 사용하면 된다.

특성 다루기

  • CLR은 형식이나 멤버, 어셈블리의 메타자료에 추가 정보를 부여하는 수단을 제공한다. 특성(attribute)이 바로 그것이다. 특성은 직렬화나 보안 같은 CLR의 여러 긴으을 설정, 제어하는 메커니즘으로 쓰인다. 그런 만큼 특성은 응용 프로그램의 필수적인 한 부분이다.
  • 특성의 핵심적인 특징 하나는 프로그래머가 직접 특성을 ㅈ가성해서 기존의 다른 특성들과 마찬가지 방법으로 사용할 수 있다는 점이다.
    • 특성의 주된 용도는 코드 요소에 추가적인 정보를 ‘부착’ 또는 ‘장식’ 하는 것이다.
    • 컴파일러는 그러한 추가 정보를 어셈블리에 포함하며, 응용 프로그램은 실행시점에서 반영 기능을 이용해서 추가 정보를 얻을 수 있다.
    • 이러한 능력을 활용하면 자동화된 단위 검사처럼 선언적으로 작동하는 서비스를 구축할 수 있다.

특성의 기초

  • 특성은 다음 세 종류로 나뉜다.
    • 비트 대응 특성
    • 커스텀 특성
    • 유사 커스텀 특성
  • 이중 확장 가능한 특성은 커스텀 특성 뿐이다.
  • ‘특성’이라는 용어 자체는 이 세 특성을 아우르지만, C# 세계에서 특성이라고 하면 대부분은 커스텀 특성이나 유사 특성을 뜻한다.
  • 비트 대응 특성(bit-mapped attribute)은 형식의 메타자료의 특정 비트에 대응된다.
    • public이나 abstract, sealed 같은 C#의 수정자 키워드들은 대부분 이런 비트 대응 특성으로 컴파일된다.
    • 메타자료 안에서 최소한의 공간만 차지한다는 점에서 (보통은 비트 하나) 그리고 CLR이 이 특성들을 찾는데 간접 접근 과정이 전혀 또는 거의 필요하지 않다는 점에서, 이 특성들은 아주 효율적이다.
    • 반영 API는 Type(그리고 기타 MemberInfo 파생 클래스들)의 개별 속성을 통해서 비트 대응 특성들을 노출한다. 이를테면 IsPublic이나 IsAbstract, IsSealed 등이 그런 속성이다.
    • 또한 Attributes 속성은 이들 대부분을 담은 플래그 열거형 값을 돌려준다.
static void Main()
{
  TypeAttributes ta = typeof(Console).Attributes;
  MethodAttributes ma = MethodInfo.GetCurrentMethod().Attributes;
  Console.WriteLine(ta + "\r\n" + ma);
}
  • 다음은 이 코드의 출력이다.
AutoLayout, AnsiClass, Class, Public, Abstract, Sealed, BeforeFieldInit, PrivateScope, Private, Static, HideBySig
  • 이와는 대조적으로 커스텀 특성(custom attribute)은 형식의 주 메타자료 테이블 바깥에 이진 자료의 형태로 존재한다. 모든 커스텀 특성은 System.Attribute의 특정 파생 클래스로 대표되며, 비트 대응 특성과는 달리 확장이 가능하다.
    • 메타 자료에 있는 이진 특성 자료에는 해당 특성 클래스를 알려주는 정보가 들어 있으며, 코드 요소에 특성을 적욯라 때 지정한 위치 인수 또는 명명된 인수들의 값들도 들어 있다.
    • 독자가 직접 정의한 커스텀 특성의 구조는 .NET Framework에 정의되어 있는 기존 특성들의 구조와 동일하다.
  • 커스텀 특성을 C#의 형식이나 멤버에 부착하는 방법은 이미 살펴 보았다. 다음은 미리 정의된 Obsolete 특성을 Foo 클래스에 부착하는 예이다.
[Obsolete] public class Foo { ... }
  • 이렇게 하면 컴파일러는 ObsoleteAttribute의 한 인스턴스를 Foo의 메타자료에 포함시킨다. 실행시점에서는 Type 또는 MemberInfo 객체에 대해 GetCustomAttributes를 호출해서 그 인스턴스에 접근할 수 있다.
  • 유사 커스텀 특성(pseudocustom attribute)은 겉으로 보기에는 보통의 커스텀 특성과 다를 바 없다. 유사 커스텀 특성 역시 System.Attribute의 특정 파생 클래스로 대표되며, 보통의 커스텀 특성과 마찬가지 방법으로 코드 요소에 적용한다. 다음이 그러한 예이다.
[Serializable] public class Foo { ... }
  • 보통의 커스텀 특성과 다른점은 최적화이다. 컴파일러나 CLR은 내부적으로 유사 커스텀 특성을 비트 대응 특성으로 변환한다. [Serializable]이나 [StructLayout], [In], [Out]이 유사 커스텀 특성의 예이다.
    • 반영 API는 IsSerializable 같은 개별 속성을 통해서 유사 특성들을 노출하며, 많은 경우 GetCustomAttribute를 호출해서 유사 특성에 대한 System.Attribute 객체를 얻을 수 있다([SerializableAttribute] 포함)
    • 따라서 프로그램의 관점에서는 유사 커스텀 특성과 보통 커스텀 특성의 차이를 무시해도 무방하다(두드러진 예외는 Reflection.Emit를 이용해서 실행시점에서 동적으로 형식을 생성할 때이다)

AttributeUsage 특성

  • AttributeUsage는 특성 클래스에 적용하는 특성이다. 이 특성은 대상 특성의 사용 방법을 컴파일러에게 알려준다.
public sealed class AttributeUsageAttribute : Attribute
{
  public AttributeUsageAttribute (AttributeTargets validOn);

  public bool AllowMultiple { get; set; }
  public bool Inherited { get; set; }
  public AttributeTargets ValidOn { get; }
}
  • AllowMultiple 속성은 주어진 특성을 같은 코드 요소에 여러 번 적용할 수 있는지의 여부를 결정한다.
    • Inherited 속성은 기반 클래스에 적용한 특성이 파생 클래스들에도 적용되는지(메서드의 경우에는 가상 메서드에 적용한 특성이 재정의 메서드에도 적용되는지)의 여부를 결정한다.
    • ValidOn 속성은 주어진 특성을 적용할 수 있는 대상들(클래스, 인터페이스, 속성, 메서드, 매개변수 등)의 집합을 결정한다.
    • 이 속성은 AttributeTargets 열거형 멤버들의 임의의 조합을 받는다. AttributeTargets 열거형의 멤버들은 다음과 같다.
All Delegate GenericParameter Parameter
Assembly Enum Interface Property
Class Event Method ReturnValue
Constructor Field Module Struct

 

  • 한 예로 .NET Framework 작성자들은 Serializable 특성 클래스에 [Attribute Usage] 특성을 다음과 같이 적용했다.
[AttributeUsage (AttributeTargets.Delegate | AttributeTargets.Enum | AttributeTargets.Struct | AttributeTargets.Class, Inherited = false)]
public sealed class SerializableAttribute : Attribute { }
  • 사실 이것이 Serializable 특성 클래스의 정의의 거의 전부이다. 속성이나 특별한 생성자가 없는 특성 클래스를 작성하기란 이렇게나 쉽다.

나만의 특성 정의

  • 다음은 독자가 직접 특성을 작성하는 방법이다.
    1. System.Attribute 클래스 또는 System.Attribute의 한 파생 클래스를 상속하는 클래스를 작성한다. 그런 클래스에는 ‘Attribute’로 끝나는 이름을 붙이는 것이 관례이다.
    2. 앞의 설명을 참고해서 [AttributeUsage] 특성을 적용한다.
      • 특성에 속성이나 생성자, 인수 등이 필요하지 않다면 이것으로 끝이다.
    3. 필요하다면 하나나 그 이상의 공용 생성자들을 작성한다. 생성자의 매개변수들은 특성의 위치 변수들을 정의한다. 이들은 특성 적용 시 반드시 지정해야 하는 필수 매개변수들이 된다.
    4. 지원하고자 하는 명명된 매개변수 각각에 대해 공용 필드 또는 속성을 선언한다. 명명된 매개변수는 특성 적용시 선택적 매개변수가 된다.
  • 특성의 속성들과 생성자 매개변수들은 반드시 다음 형식 중 하나이어야 한다.
    • 봉인된(sealed) 기본 형식, 즉 bool이나 byte, char, double, float, int, long, short, string
    • Type 형식
    • 열거형
    • 위의 형식들의 1차원 배열
  • 또한 특성 적용시 컴파일러가 속성 또는 생성자 인수 각각을 정적으로 평가할 수 있어야 한다.
  • 한 예로 다음은 자동화된 단위 검사를 돕는 [Test] 특성을 정의하는 클래스이다. 이 특성은 메서드가 검사 대상임을 지정하는데 쓰인다. 특성 인수들을 통해서 검사 되풀이 횟수와 검사 실패 시 표시할 메시지를 지정할 수도 있다.
[AttributeUsage (AttributeTargets.Method)]
public sealed class TestAttribute : Attribute
{
  public int Repetitions;
  public string FailureMessage;
  public TestAttribute() : this(1) { }
  public TestAttribute(int repetitions) { Repetitions = repetitions; }
}
  • 다음은 Foo 클래스의 여러 메서드들에 이 [Test] 특성을 다양한 방식으로 적용한 예이다.
class Foo
{
  [Test]
  public void Method1() { ... }

  [Test(20)]
  public void Method2() { ... }

  [Test(20, FailureMessage="버그를 잡으세요!")]
  public void Method3() { ... }
}

실행시점에서 특성 조회

  • 실행시점에서 특성을 조회하는 표준적인 방법은 다음 두 가지이다.
    • 임의의 Type 또는 MemberInfo 객체에 대해 GetCustomAttributes 메서드를 호출한다.
    • Attribute.GetCustomAttribute 메서드나 Attribute.GetCustomAttributes 메서드를 호출한다.
  • 후자의 두 메서드는 유효한 특성 대상들에 대응되는 임의의 반영 객체(Type, Assembly, Module, MemberInfo, ParameterInfo)를 받도록 중복적재되어 있다.
  • .NET Framework 4.0부터는 형식 또는 멤버 정보 객체에 대해 GetCustomAttributesData 메서드를 호출해서 특성 정보를 얻을 수도 있다.
    • GetCustomAttributes 메서드와의 차이는 이 메서드는 특성을 인스턴스화 하는 방법을 알려준다는 점이다.
    • 이 메서드가 돌려주는 객체로부터 특성을 적용할 떄 쓰인 생성자 중복적재 버전과 생성자 매개변수들 및 명명된 매개변수들에 지정된 값들을 알아낼 수 있다.
    • 이는 적용 당시와 같은 상태의 특성을 재국축하는 코드 또는 IL을 산출하려 할 때 유용하다.
  • 다음은 앞의 Foo 클래스의 메서드 중 [Test] 특성(TestAttribute 클래스)이 적용된 메서드를 나열하는 방법을 보여주는 예이다.
foreach (MethodInfo mi in typeof (Foo).GetMethods())
{
  TestAttribute att = (TestAtrribute) Attribute.GetCustomAttribute(mi, typeof(TestAttribute));

  if (att != null)
    Console.WriteLine("{0} 메서드는 검사 대상임: 반복 횟수={1}; 메시지={2}", mi.Name, att.Repetitions, att.FailureMessage);
}

// 출력
// Method1 메서드는 검사 대상임: 반복 횟수=1; 메시지=
// Method2 메서드는 검사 대상임: 반복 횟수=20; 메시지=
// Method3 메서드는 검사 대상임: 반복 횟수=20; 메시지=버그를 잡으세요!
  • 예제를 마무리하는 차원에서 다음은 이 특성을 이용해서 단위 검사 시스템을 작성하는 방법을 엿볼 수 있는 코드이다. 이 코드는 [Test] 특성이 부착된 메서드를 실제로 호출한다.
foreach (MethodInfo mi in typeof (Foo).GetMethods())
{
  TestAttribute att = (TestAttribute) Attribute.GetCustomAttribute(mi, typeof(TestAttribute));

  if (att != null)
  {
    for (int i = 0; i < att.Repetitions; i++)
    {
      try
      {
        mi.Invoke(new Foo(), null);  // 인수 없이 메서드 호출
      }
      catch (Exception ex)  // att.FailureMessage를 포함한 예외를 발생
      {
        throw new Exception("오류: " + att.FailureMessage, ex);
      }
    }
  }
}
  • 특성의 반영이라는 주제로 돌아가서, 다음은 주어진 한 형식에 부여된 특성들을 나열하는 예이다.
[Serializable, Obsolete]
class Test
{
  static void Main()
  {
    object[] atts = Attribute.GetCustomAttributes(typeof(Test));
    foreach (object att in atts) 
      Console.WriteLine(att);
  }
}

// 출력
// System.ObsoleteAttribute
// System.SerializableAttribute

반영 전용 문맥에서 특성 조회

  • 반영 전용 문맥에 적재된 멤버에 대해 GetCustomAttributes를 호출하는 것은 금지되어 있다. 이유는 그런 멤버에 대한 특성들을 조회하려면 임의의 형식의 특성들을 인스턴스화 해야 하기 때문이다(앞서 이야기했듯이 반영 전용 문맥에서는 객체의 인스턴스화가 허용되지 않는다)
  • 이 문제를 피하기 위해 .NET Framework는 그런 특성의 반영에 특화된 CustomAttributeData라는 형식을 제공한다. 다음은 이 형식을 사용하는 예이다.
IList<CustomAttributeData> atts = CustomAttributeData.GetCustomAttributes(myReflectionOnlyType);
foreach (CustomAttributeData att in atts)
{
  Console.Write(att.GetType());  // Attribute type
  Console.WriteLine(" " + att.Constructor);  // ConstructorInfo object

  foreach (CustomAttributeTypedArgument arg in att.ConstructorArguments)
    Console.WriteLine(" " + arg.ArgumentType + "=" + arg.Value);

  foreach (CustomAttributeNamedArgument arg in att.NamedArguments)
    Console.WriteLine(" " + arg.MemberInfo.Name + "=" + arg.TypedValue);
}
  • 특성 클래스가 반영을 적용할 형식과는 다른 어셈블리에 들어 있는 경우가 많다. 그로부터 비롯되는 문제를 해결하는 한 가지 방법은 현재 응용 프로그램 도메인에 대한 ReflectionOnlyAssemblyResolve 이벤트를 처리하는 것이다.
ResolveEventHandler handler = (object sender, ResolveEventArgs ars) => Assembly.ReflectoinOnlyLoad(args.Name);
AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += handler;

// 특성들에 대한 반영을 수행...

AppDomain.CUrrentDomain.ReflectionOnlyAssemblyResolve -= handler;

동적 코드 생성

  • System.Reflection.Emit 이름공간에는 실행시점에서 메타자료와 IL 코드를 생성하기 위한 클래스들이 들어 있다. 프로그래밍 과제 중에는 코드를 동적으로 생성하는 것이 유용한 것들이 있다.
    • 예컨대 정규 표현식 API는 특정 정규식에 맞게 조율된, 성능 좋은 형식들을 산출한다.
    • .NET Framework는 또한 Remoting을 위해 투명한 프록시를 동적으로 생성하거나 실행시점 부담을 최소화하면서 특정 XSLT 변환을 수행하는 것을 비롯한 여러 용도로 Reflection.Emit을 활용한다.
    • LINQPad는 형식 있는 DataContext 클래스를 동적으로 생성하는 용도로 Reflection.Emit을 사용한다.
    • Windows 스토어 프로파일은 Reflection.Emit을 지원하지 않는다.

DynamicMethod를 이용한 IL 코드 생성

  • System.Reflection.Emit 이름공간의 DynamicMethod 클래스는 즉석에서 메서드를 생성하는데 사용하는 가벼운 도구이다. TypeBuilder와는 달리, 이 클래스를 사용할 때는 메서드를 담을 동적 어셈블리나 모듈, 형식을 미리 준비할 필요가 없다. 그래서 이 클래스는 간단한 작업에 적합하며, Reflection.Emit의 세계에 입문하는 용도로 좋다.
  • 더 이상 참조되지 않는 DynamicMethod 객체와 관련 IL 코드는 쓰레기 수거의 대상이다. 이는 동적 메서드를 거듭 생성해도 메모리가 고갈되지는 않음을 뜻한다.
    • 동적 어셈블리에도 그런 식으로 쓰레기 수거가 적용되게 하려면 어셈블리 생성시 AssemblyBuilderAccess.RunAndCollect 플래그를 지정해야 한다.
  • 다음은 DynamicMethod의 간단한 용범을 보여주는 예제이다. 이 예제는 콘솔에 Hello World를 출력하는 메서드를 생성한다.
public class Test
{
  static void Main()
  {
    var dynMeth = new DynamicMethod("Foo", null, null, typeof(Test));
    ILGenerator gen = dynMeth.GetILGenerator();
    gen.EmitWriteLine("Hello World");
    gen.Emit(OpCodes.Ret);
    dynMeth.Invoke(null, null);  // Hello World
  }
}
  • OpCodes는 모든 IL 옵코드(opcode, 특정 종류의 연산을 나타내는 부호)를 담은 정적 읽기 전용 필드이다.
    • 동적 코드 생성과 관련된 기능성은 대부분 다양한 옵코드를 통해서 노출되지만, ILGenerator는 이름표(label)와 지역 변수의 생성에 특화된 메서드들과 예외 처리에 특화된 메서드들도 제공한다. 하나의 메서드는 항상 OpCodes.Ret으로 끝난다.
    • OpCodes.Ret의 Ret는 ‘return’을 줄인 것인데, 이 옵코드는 return 문 또는 메서드의 끝에 도달한 경우 뿐만 아니라 다른 어떤 분기/예외 발생 명령에 의해 함수에서 벗어날 때에도 쓰인다.
    • ILGenerator의 EmitWriteLine 메서드는 여러 저수준 옵코드를 Emit를 이용해서 산출하는 과정을 한 번에 수행하는 단축 메서드이다. 위 예제의 EmitWriteLine 호출을 다음 코드로 대체해도 같은 결과가 나온다.
MethodInfo writeLineStr = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });
gen.Emit(OpCodes.Ldstr, "Hello World");  // 문자열을 적재한다.
gen.Emit(OpCodes.Call, writeLineStr);  // 메서드를 호출한다.
  • DynamicMethod의 생성자에 typeof(Test)를 넘겨 주었음을 주목하기 바란다. 이렇게 하면 동적 메서드가 그 형식의 비공용메서드에 접근할 수 있으며, 그래서 다음과 같은 코드가 가능해 진다.
public class Test
{
  static void Main()
  {
    var dynMeth = new DynamicMethod("Foo", null, null, typeof(Test));
    ILGenerator gen = dynMeth.GetILGenerator();

    MethodInfo privateMethod = typeof(Test).GetMethod("HelloWorld", BindingFlags.Static | BindingFlags.NonPublic);

    gen.Emit(OpCodes.Call, privateMethod);  // HelloWorld 메서드를 호출
    gen.Emit(OpCodes.Ret);

    dynMeth.Invoke(null, null);  // Hello World
  }

  static void HelloWolrd()  // 이런 전용 메서드도 호출할 수 있다.
  {
    Console.WriteLine("Hello World");
  }
}
  • IL을 이해하려면 상당한 시간을 투자해야 한다. 모든 옵코드를 이해하려 드는 대신, C# 프로그램 하나를 컴파일해서 IL 코드를 조사, 복사, 조정해 보는 것이 훨씬 쉽다.
    • LINQPad는 임의의 메서드 또는 사용자가 입력한 코드 조각의 IL을 표시해준다. 기존 어셈블리를 조사하는데는 ildasm이나 .NET Reflector 같은 어셈블리 표시 도구가 유용하다.

평가 스택

  • IL 핵심부에는 평가 스택(evaluation stack)이라는 개념이 놓여 있다. 인수들을 지정해서 메서드를 호출할 때는 먼저 그 인수들을 평가 스택에 넣고(적재) 메서드를 호출한다.
    • 메서드 안에서는 평가 스택에서 필요한 인수들을 뽑는다. 앞에서 Console.WriteLine을 호출할 때 실제로 이런 방법을 사용했었다.
    • 다음은 그와 비슷한 예제로 이번에는 정수 인수를 사용한다.
var dynMeth = new DynamicMethod("Foo", null, null, typeof(void));
ILGenerator gen = dynMeth.GetILGenerator();
MethodInfo writeLineInt = typeof(Console).GetMethod("WriteLine", new Type[] { typeof (int) });

// 다양항 형식과 크기의 수치 리터럴을 적재하는 Ldc* 옵코드들이 마련되어 있다.
gen.Emit(OpCodes.Ldc_I4, 123);  // 4바이트 정수를 스택에 넣는다.
gen.Emit(OpCodes.Call, writeLineInt);
gen.Emit(OpCodes.Ret);

dynMeth.Invoke(null, null);  // 123
  • 두 수를 더할 떄는 먼저 각 수를 평가 스택에 적재한 후 Add 옵코드를 호출한다. Add 옵코드는 평가 스택에서 두 값을 뽑아서 더한 결과를 다시 평가 스택에 넣는다.
    • 다음은 2와 2를 더하고 그 결과를 앞에서 얻은 writeLine 메서드를 이용해서 출력하는 예이다.
gen.Emit(OpCodes.Ldc_I4, 2);  // 값이 2dls 4바이트 정수를 스택에 넣는다.
gen.Emit(OpCodes.Ldc_I4, 2);  // 값이 2dls 4바이트 정수를 스택에 넣는다.
gen.Emit(OpCodes.Add);  // 두 값을 더한다.
gen.Emit(OpCodes.Call, writeLineInt);
  • 다음과 같은 계산도 가능하다.
gen.Emit(OpCodes.Ldc_I4, 1);
gen.Emit(OpCodes.Ldc_I4, 10);
gen.Emit(OpCodes.Ldc_I4, 2);
gen.Emit(OpCodes.Div);
gen.Emit(OpCodes.Add);
gen.Emit(OpCodes.Call, writeLineInt);

동적 메서드에 인수 전달

  • 동적 메서드에 전달할 인수를 스택에 넣을 떄는 Ldarg나 Ldarg_XXX 옵코드들을 사용한다.
    • 메서드에서 값을 반환하려면, 정확히 하나의 값을 스택에 넣은 후 메서드를 종료하면 된다. 이를 위해서는 DynamicMethod의 생성자를 호출할 때 반환 형식과 인수 형식들을 지정해야 한다.
    • 다음은 두 정수의 합을 돌려주는 동적 메서드를 생성하는 예이다.
var dynMeth = new DynamicMethod("Foo", 
  typeof(int),  // 반환 형식: int
  new[] { typeof (int), typeof(int) },  // 매개변수 형식들: int, int
  typeof (void));

ILGenerator gen = dynMeth.GetILGenerator();

gen.Emit(OpCodes.Ldarg_0);  // 첫 인수를 평가 스택에 넣고
gen.Emit(OpCodes.Ldarg_1);  // 둘째 인수를 평가 스택에 넣고
gen.Emit(OpCodes.Add);  // 첫 인수를 평가 스택에 넣고
gen.Emit(OpCodes.Ret);  // 스택에 값이 하나 있는 상태에서 반환

int result = (int) dynMeth.Invoke(null, new object[] { 3, 4 } );  // 7
  • 메서드 종료시 평가 스택에는 반드시 정확히 하나 또는 0개(메서드의 값 반환 여부에 따라)의 항목이 있어야 한다. 이를 위반하면 CLR은 메서드의 실행을 거부한다. 평가 스택의 한 항목을 처리 없이 제거하려면 OpCodes.Pop을 사용하면 된다.
  • Invoke를 호출하는 대신, 동적 메서드를 형식 있는 대리자로 취급하면 다루기가 훨씬 편하다. 그런 용도의 메서드가 CreateDelegate이다.
    • 예컨대 BinaryFuntion이라는 대리자가 정의되어 있다고 하면,
delegate int BinaryFunction(int n1, int n2);
  • 앞의 예제의 마지막 줄을 다음으로 대체할 수 있다.
BinaryFunction f = (BinaryFunction) dynMeth.CreateDelegate(typeof(BinaryFunction));
int result = f (3, 4);  // 7
  • 대리자는 동적 메서드의 호출의 추가부담도 제거한다. 대리자를 이용하면 호출당 몇 마이크로초가 절약된다.

지역 변수 생성

  • 지역 변수는 ILGenerator 객체에 대해 DeclareLocal을 호출해서 선언할 수 있다.
    • 이 메서드가 돌려준 LocalBuilder 객체를 Ldloc(지역 변수 적재) 또는 Stloc(지역 변수 저장) 같은 옵코드와 함께 사용해서 지역 변수의 값을 조회하거나 설정한다. Ldloc은 평가 스택에 값을 넣고, Stloc은 값을 뽑는다.
    • 예컨대 다음과 같은 C# 코드를 생각해 보자.
int x = 6;
int y = 7;
x *= y;
Console.WriteLine(x);
  • 다음은 이 코드에 해당하는 IL 코드를 동적으로 생성하는 예이다.
var dynMeth = new DynamicMethod("Test", null, null, typeof(void));
ILGenerator gen = dynMeth.GetILGenerator();

LocalBuilder localX = gen.DeclareLocal(typeof(int));  // x 선언
LocalBuilder localY = gen.DeclareLocal(typeof(int));  // y 선언

gen.Emit(OpCodes.Ldc_I4, 6);  // 리터럴 6을 평가 스택에 넣는다.
gen.Emit(OpCodes.Stloc, localX);  // 그 값을 localX에 저장한다.
gen.Emit(OpCodes.Ldc_I4, 7);  // 리터럴 7을 평가 스택에 넣는다.
gen.Emit(OpCodes.Stloc, localY);  // 그 값을 localY에 저장한다.

gen.Emit(OpCodes.Ldloc, localX);  // localX의 값을 평가 스택에 넣는다.
gen.Emit(OpCodes.Ldloc, localY);  // localY의 값을 평가 스택에 넣는다.
gen.Emit(OpCodes.Mul);  // 두 값을 곱한다.
gen.Emit(OpCodes.Stloc, localX);  // 결과를 localX에 저장한다.

gen.EmitWriteLine(localX);  // localX의 값을 출력한다.
gen.Emit(OpCodes.Ret);

dynMeth.Invoke(null, null); // 42
  • Redgate사의 .NET Feflector는 동적 메서드에서 오류를 찾는데 아주 좋은 도구이다. IL 코드를 C# 코드로 역컴파일해 보면 잘못된 지점이 확연히 드러나는 경우가 많다.

분기

  • IL에는 while이나 do, for 루프 같은 것이 없다. 모든 루프는 이름표(label)와 goto문 또는 조건부 goto문에 해당하는 옵코드로 구현된다.
    • 분기(branching)를 위한 옵코드로는 Br(무조건 분리), Brtrue(평가 스택의 값이 true이면 불가), Blt(첫 값이 둘째 값보다 작으면 분기)가 있다.
  • 분기 대상을 설정하려면 먼저 DefineLabel을 호출해서 Label 객체를 만들고, 이름표를 붙일 지점에서 그 객체로 MarkLabel을 호출하면된다.
    • 예컨대 다음과 같은 C# 코드를 생각해 보자.
int x = 5;
while (x < 10)
  Console.WriteLine(x++);
  • 다음은 이에 해당하는 IL 코드를 산출하는 예이다.
ILGenerator gen = ...

Label startLoop = gen.DefineLabel();  // 이름표들을 선언
Label endLoop = gen.DefineLabel();

LocalBuilder x = gen.DeclareLocal(typeof(int));  // int x
gen.Emit(OpCodes.Ldc_I4, 5);
gen.Emit(OpCodes.Stloc, x);  // x = 5

gen.MarkLabel(startLoop);
  gen.Emit(OpCodes.Ldc_I4, 10);  // 10을 평가 스택에 적재
  gen.Emit(OpCodes.Ldloc, x);  // x를 평가 스택에 적재

  gen.Emit(OpCodes.Blt, endLoop);  // if (x > 10) goto endLoop

  gen.EmitWriteLine(x);  // Console.WriteLine(x)

  gen.Emit(OpCodes.Ldloc, x);  // x를 평가 스택에 적재
  gen.Emit(OpCodes.Ldc_I4, 1);  // 1을 평가 스택에 적재
  gen.Emit(OpCodes.Add);  // 그 둘을 더하고
  gen.Emit(OpCodes.Stloc, x);  // 그 결과를 다시 x에 저장

  gen.Emit(OpCodes.Br, startLoop);  // 루프의 시작으로 돌아간다.
gen.MarkLabel(endLoop);

gen.Emit(OpCodes.Ret);

객체 인스턴스화와 인스턴스 메서드 호출

  • new에 해당하는 IL 옵코드는 Newobj이다. 이 옵코드는 주어진 생성자 정보를 이용해서 객체를 생성하고 그 객체를 평가 스택에 넣는다. 다음은 StringBuilder 객체를 생성하는 예이다.
var dynMeth = new DynamicMethod("Test", null, null, typeof(void));
ILGenerator gen = dynMeth.GetILGenerator();

ConstructorInfo ci  = typeof(StringBuilder).GetConstructor(new Type[0]);
gen.Emit(OpCodes.Newobj, ci);
  • 일단 평가 스택에 객체가 적재되면, Call 옵코드나 Callvirt 옵코드를 이용해서 그 객체의 인스턴스 메서드를 호출할 수 있다.
    • 앞의 예제에 이어서 다음은은 StringBuilder의 MaxCapacity 속성을 조회하는 예이다. 이 예제는 MaxCapacity의 get 접근자를 호출한 결과를 출력한다.
gen.Emit(OpCodes.Callvirt, typeof(StringBuilder).GetProperty("MaxCapacity").GetGetMethod());
gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(int) } ));
gen.Emit(OpCodes.Ret);
bynMeth.Invoke(null, null);  // 2147483647
  • IL에서 C#의 호출 의미론을 흉내 내려면
    • 정적 메서드와 값 형식 인스턴스 메서드의 호출에는 Call 옵코드를
    • 참조 형식 인스턴스 메서드의 호출에는 Callvirt를 사용한다(가상 선언부와 무관하게)
  • 지금 예제에서는 StringBuilder 인스턴스에 대해 Callvirt를 사용했다. Max Property가 가상이 아님에도 그렇게 했음을 주목하기 바란다.
    • 이렇게 해도 오류가 나지는 않는다. 그냥 비가상 호출이 수행될 뿐이다.
    • 참조 형식 인스턴스 메서드를 호출할 때 항상 Callvirt를 사용하면 그 반대의 경우, 즉 가상 메서드를 Call로 호출하는 위험을 피할 수 있다 (이것은 실제로 ‘위험’이다. 대상 메서드의 작성자가 나중에 메서드의 선언을 바꿀 수도 있기 때문이다.)
    • 또한 Callvirt에는 대상 메서드가 널이 아닌지를 점검한다는 장점도 있다.
  • 가상 메서드를 Call로 호출하면 가상 호출 의미론을 우회해서 그 메서드를 직접 호출하게 된다. 이것이 바람직한 경우는 드물며, 실제로 이는 형식 안전성을 위반하는 일이다.
  • 다음 예제는 인수 두 개를 전달해서 StringBuilder 객체를 생성하고, 거기게 문자열 “, world!”를 추가한 후 ToString을 호출한다.
ConstructorInfo ci  = typeof(StringBuilder).GetConstructor(new[] { typeof(string), typeof(int) });
gen.Emit(OpCodes.Ldstr, "Hello");  // 문자열을 평가 스택에 적재
gen.Emit(OpCodes.Ldc_I4, 1000);  // 정수 값을 평가 스택에 적재
gen.Emit(OpCodes.Newobj, ci);  // StringBuilder 객체를 생성

Type[] strT = { typeof(string) };
gen.Emit(OpCodes.Ldstr, ", world!");
gen.Emit(OpCodes.Call, typeof(StringBuilder).GetMethod("Append", strT));
gen.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString"));
gen.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", strT));
gen.Emit(OpCodes.Ret);
bynMeth.Invoke(null, null);  // Hello, world!
  • 이 예제는 재미 삼아  typeof(object)에 대해 GetMethod를 호출한 후 Callvirt를 이용해서 ToString에 대한 가상 메서드 호출을 수행하지만, 다음처럼 StringBuilder 형식 자체에 대해 ToString를 호출해도 같은 결과를 얻을 수 있다.
gen.Emit(OpCodes.Callvirt, typeof(StringBuilder).GetMethod("ToString", new Type[0]));
  • GetMethod 호출에서 빈 Type 배열은 꼭 필요하다. StringBuilder에 다른 형식을 받는 ToString 중복적재 버전들이 있기 때문이다.
  • object의 ToString을 다음처럼 비가상으로 호출한다면, System.Text.StringBuilder가 출력되었을 것이다. 다른 말로 하면, 이 비가상 호출은 StringBuilder의 ToString 중복적재를 건너뛰고 object의 버전을 직접 실행한다.
gen.Emit(OpCodes.Call, typeof(object).GetMethod("ToString"));

예외 처리

  • ILGenerator는 예외 처리에 특화된 메서드를 제공한다. 다음과 같은 C# 코드를 생각해 보자.
try
{
  throw new NotSupportedException();
}
catch (NotSupportedException ex)
{
  Console.WriteLine(ex.Message);
}
finally
{
  Console.WriteLine("Finally");
}
  • 이를 IL로 옮기려면 다음과 같이 하면 된다.
MethodInfo getMessageProp = typeof(NotSupportedException).GetProperty("Message").GetGetMethod();
MethodInfo wirteLineString = typeof(Console).GetMethod("WriteLine", new[] { typeof(object) });

gen.BeginExceptionBlock();
  ConstructorInfo ci  = typeof(NotSupportedException).GetConstructor(new Type[0]);
  gen.Emit(OpCodes.Newobj, ci);
  gen.Emit(OpCodes.Throw);
gen.BeginCatchBlock(typeof(NotSupportedException));
  gen.Emit(OpCodes.Callvirt, getMessageProp);
  gen.Emit(OpCodes.Call, writeLineString);
gen.BeginFinallyBlock();
    gen.EmitWriteLine("Finally");
gen.EndExceptionBlock();
  • C# 에서처럼 하나의 try 블록에 여러 개의 catch 절을 붙일 수 있다. 같은 예외를 다시 던지려면 Rethrow 옵코드를 산출하면 된다.
  • ILGenerator는 ThrowException이라는 보조 메서드를 제공한다. 그런데 이 메서드에는 버그가 있어서 DynamicMethod와는 사용할 수 없다. 이 메서드는 MethodBuilder만 지원한다.

어셈블리와 형식의 산출

  • DynamicMethod가 편리하긴 하지만, 메서드만 생성할 수 있다는 한계가 있다. 다른 코드 요소를 산출하려면 또는 하나의 완성된 형식을 동적으로 생성하려면 완전한 ‘중량급’ API를 사용해야 한다.
    • 하나의 완성된 형식을 동적으로 생성하려면 어셈블리와 모듈도 동적으로 생성해야 한다. 단, 그런 어셈블리가 반드시 디스크에 존재할 필요는 없다. 전적으로 메모리에만 존재하는 어셈블리도 가능하다.
  • 하나의 형식을 동적으로 구축한다고 하자. 형식은 반드시 어떤 모듈에 속해야 하고, 모듈은 반드시 어떤 어셈블리에 속해야 한다.  따라서 형식을 만들려면 먼저 어셈블리와 모듈을 만들어야 한다.
    • 그 둘을 위한 .NET Framework의 클래스는 각각 AssemblyBuilder와 ModuleBuilder이다.
AppDomain appDomain = AppDomain.CurrentDomain;
AssemblyName aname = new AssemblyName("MyDynamicAssembly");
AssemblyBuilder assemBuilder = appDomain.DefineDynamicAssmbly(aname, AssemblyBuilderAccess.Run);
ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule("DynModule");
  • 일단 생성된 어셈블리는 더 이상 변경할 수 없으므로, 기존의 어셈블리에 형식을 추가할 수는 없다.
    • 동적 어셈블리는 기본적으로 쓰레기 수거의 대상이 아니며, 응용 프로그램 도메인이 끝날 때까지 메모리에 상주한다. 단, 어셈블리를 정의할 때 AssemblyBuilderAccess.RunAndCollect를 지정하면 쓰레기 수거 대상이 된다.
    • 어셈블리의 쓰레기 수거에는 다양한 제약이 따른다.
  • 형식을 담은 어셈블리와 모듈을 만든 다음에는 TypeBuilder를 이용해서 형식을 만든다.
    • 예컨대 다음은 Widget이라는 클래스를 정의한다.
TypeBuilder tb = modBuilder.DefineType("Widget", TypeAttributes.Public);
  • TypeAttributes 플래그 열거형에는 C#의 형식을 ildasm으로 역어셈블 했을 때 볼 수 있는 CLR 형식 수정자들에 대응되는 멤버들이 정의되어 있다.
    • 이를테면 Abstract와 Sealed 같은 멤버들이 있으며, 또한 .NET 인터페이스를 정의하는데 사용하는 Interface라는 멤버도 있다.
    • 또한 C#에서 [Serializable] 특성을 적용하는 것에 해당하는 Serializable 멤버와 [StructLayout[LayoutKind.Explicit)] 특성을 적용하는 것에 해당하는 Explicit라는 멤버도 있다.
  • DefineTYpe 메서드 호출 시 기반 형식을 지정할 수도 있다.
    • 구조체를 정의할 때는 System.ValueType을 기반 형식으로 지정한다.
    • 대리자를 정의할 때는 System.MulticastDelegate를 기반 형식으로 지정한다.
    • 인터페이스들을 구현하는 클래스를 정의할 때는 원하는 인터페이스 형식들의 배열을 받는 생성자를 사용한다.
    • 인터페이스를 정의할 때는 TypeAttributes.Interface | TypeAttributes.Abstract를 지정한다.
  • 이제 형식에 멤버를 추가할 수 있다.
MethodBuilder methBuilder = tb.DefineMethod("SayHello", MethodAttributes.Public, null, null);
ILGenerator gen = methBuilder.GetILGenerator();
gen.EmitWriteLine("Hello world");
gen.Emit(OpCodes.Ret);
  • 멤버들을 다 추가한 후에는 반드시 CreateType 메서드를 호출해야 한다. 그래야 형식의 정의가 완성된다.
Type t = tb.CreateType();
  • 형식의 정의를 완성한 다음부터는 통상적인 반영 기법을 이용해서 형식을 조사하고 동적 바인딩을 수행할 수 있다.
object o = Activator.CreateInstance(t);
t.GetMethod("SayHello").Invoke(o, null);  // Hello world

산출된 어셈블리의 저장

  • AssemblyBuilder 객체에 대한 Save 메서드는 동적으로 생성한 어셈블리를 지정된 이름의 파일에 기록한다. 이러한 저장이 성공하려면 그 전에 다음 두 가지 처리를 해주어야 한다.
    • AssemblyBuilder 객체를 생성할 때 AssemblyBuilderAccess.Save 또는 AssemblyBuilderAccess.RunAndSave를 지정한다.
    • ModuleBuilder 객체를 생성할 때 파일 이름을 지정한다(다중 모듈 어셈블리를 만드는 것이 아닌 한 이 파일 이름은 어셈블리의 파일 이름과 일치해야 한다)
  • 필요하다면 AssemblyName 객체의 Version이나 KeyPair(서명을 위해) 같은 속성을 설정할 수도 있다.
  • 다음 예를 보자
AppDomain appDomain = AppDomain.CurrentDomain;
AssemblyName aname = new AssemblyName("MyEmissions");
aname.Version = new Version(2, 13, 0, 1);

AssemblyBiulder assemBuilder = domain.DefineDynamicAssembly(aname, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule("MainModule", "MyEmissions.dll");

// 이전에 했던 것처럼 형식들을 생성한다.
/ ...

assemBuilder.Save("MyEmissions.dll");
  • 이 코드는 어셈블리를 응용 프로그램의 기반 디렉터리에 저장한다. 다른 장소에 저장하려면 AssemblyBuilder 생성시 다음처럼 다른 디렉터리를 지정해야 한다.
AssemblyBiulder assemBuilder = domain.DefineDynamicAssembly(aname, AssemblyBuilderAccess.RunAndSave, @"d:\aeemblies");
  • 동적 어셈블리를 파일에 저장하면 다른 어셈블리들 같은 보통의 어셈블리가 된다. 앞에서 생성한 엇헴블리를 파일로 저장했다면, 프로그램에서 정적으로 참조해서 다음처럼 어셈블리의 형식을 직접 사용할 수 있다.
Widget w = new Widget();
w.SayHello();

Reflection.Emit 이름공간의 객체 모형

  • 아래 그림은 System.Reflection.Emit의 필수 형식들을 보여준다. 이 형식들은 각자 하나의 CLR 구축 요소에 대응되며, System.Reflection 이름공간에 있는 관련 기반 형식을 상속한다.

  • 이러한 형식들은 정적인 C# 코드에 쓰이는 모든 구축 요소들에 대응되는 동적 구축 요소들을 제공한다. 이 덕분에 어떤 코드라도 동적으로 구축(‘산출’)할 수 있다.
    • 예컨대 앞에서는 Console.WriteLine을 다음과 같이 호출했다.
MethodInfo writeLineInt = typeof(Console).GetMethod("WriteLine", new Type[] { typeof (string) });
gen.Emit(OpCodes.Call, writeLine);
  • MethodInfo 대신 MethodBuilder로 gen.Emit를 호출하는 것도 이만큼이나 쉽다.
    • 사실 같은 형식 안의 다른 메서드를 호출하는 동적 메서드를 생성하려면 MethodBuilder가 필수이다.
  • 형식을 생성해서 멤버들을 다 추가한 후에는 TypeBuilder 객체에 대해 CreateType을 호출해야 함을 기억할 것이다. CreateType을 호출하면 TypeBuilder와 모든 멤버가 봉인되어서 더 이상의 변경이 불가능해진다. CreateType은 인스턴스를 생성할 수 있는 진짜 Type 객체를 돌려준다.
  • CreateType을 호출하기 전에는 TypeBuilder 객체와 그 멤벋르이 모두 ‘미생성(uncreated)’ 상태이다. 미생성 상태의 형식에 대해서는 할 수 있는 일이 아주 제한적이다. 특히 GetMembers나 GetMethod, GetProperty처럼 MemberInfo 객체를 돌려주는 멤버는 호출할 수 없다.
    • 미생성 형식에 대해 그런 멤버를 호출하면 예외가 발생한다. 예외를 피하려면 반드시 애초에 형식에 멤버를 추가할 때 만들었던 산출 객체를 사용해야 한다.
TypeBuilder tb = ...

MethodBuilder method1 = tb.DefineMethod("Method1", ...);
MethodBuilder method2 = tb.DefineMethod("Method2", ...);

ILGenerator gen1 = method1.GetILGenerator();

// method1에서 method2를 호출해야 한다고 할 때:
gen1.Emit(OpCodes.Call, method2);  // 올바른 방법
gen1.Emit(OpCodes.Call, tb.GetMethod("Method2"));  // 잘못된 방법
  • CreateType을 호출하고 나면, CreateType이 돌려주는 Type 객체뿐만 아니라 원래의 TypeBuilder 객체에 대해서도 반영과 활성화가 가능하다.
    • 사실 CreateType을 호출하면 TypeBuilder는 실제 Type 객체에 대한 하나의 프록시가 된다.

형식 멤버 산출

  • 이번 절의 모든 예제는 TypeBuilder 형식의 객체 tb가 다음과 같이 인스턴스화 되어 있다고 가정한다.
AppDomain appDomain = AppDomain.CurrentDomain;
AssemblyName aname = new AssemblyName("MyEmissions");

AssemblyBiulder assemBuilder = domain.DefineDynamicAssembly(aname, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule("MainModule", "MyEmissions.dll");

TypeBuilder tb = modBuilder.DefineType("Widget", TypeAttributes.Public);

메서드 산출

  • DefineMethod 호출 시 메서드의 반환 형식과 매개변수 형식들을 지정할 수 있다. 방식은 DynamicMethod 객체를 인스턴스화 할 때와 같다. 예컨대 다음과 같은 C# 메서드를 동적으로 생성하려면
public static double SquareRoot(double value)
{
  return Math.Sqrt(value);
}
  • 다음과 같이 하면 된다.
ModuleBuilder mb = tb.DefineMethod("SquareRoot", 
  MethodAttributes.Static | MethodAttributes.Public, 
  CallingConventions.Standard, 
  typeof(double), // 반환 형식
  new[] { typeof(double) } );  // 매개변수 형식(들)
mb.DefineParameter(1, ParameterAttributes.None, "value");  // 이름을 배정

ILGenerator gen = mb.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);  // 첫 인수를 적재
gen.Emit(OpCodes.Call, typeof(Math).GetMethod("Sqrt"));
gen.Emit(OpCodes.Ret);

Type realType = tb.CreateType();
double x = (double) tb.GetMethod("SquareRoot").Invoke(null, new object[] { 10.0 });
Console.WriteLine(x);  // 3.162277...
  • DefineParameter 호출은 생략할 수 있다. 이 메서든느 주로 매개변수에 이름을 배정하는 용도로 쓰인다.
    • 첫 인수 1은 첫 번째 매개변수를 뜻한다(0은 반환 값이다)
    • DefineMethod를 호출하면 암묵적으로 매개변수들에 __p1, __p2 같은 이름이 배정된다.
    • 어셈블리를 디스크에 기록해서 보통의 어셈블리로 사용하는 경우에는 소비자가 쉽게 기억할 수 있는 매개변수 이름을 DefineParameter를 이용해서 명시적으로 배정하는 것이 바람직하다.
  • DefineParameter는 ParameterBuilder 객체를 돌려준다.
  • 다음으로 참조 전달 매개변수를 산출하는 방법을 살펴보자. 예컨대 다음과 같은 C# 메서드를 동적으로 생성하려면
public static double SquareRoot(ref double value)
{
  return Math.Sqrt(value);
}
  • 다음처럼 매개변수 형식(들)에 대해 MakeByeRefType을 호출하면 된다.
ModuleBuilder mb = tb.DefineMethod("SquareRoot", 
  MethodAttributes.Static | MethodAttributes.Public, 
  CallingConventions.Standard, 
  typeof(double), // 반환 형식
  new Type[] { typeof(double).MakeByRefType() } );
mb.DefineParameter(1, ParameterAttributes.None, "value");

ILGenerator gen = mb.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Lbind_R8);
gen.Emit(OpCodes.Call, typeof(Math).GetMethod("Sqrt"));
gen.Emit(OpCodes.Stind_R8);
gen.Emit(OpCodes.Ret);

Type realType = tb.CreateType();
object[] args = { 10.0 };
tb.GetMethod("SquareRoot").Invoke(null, args);
Console.WriteLine(args[0]);  // 3.162277...
  • 이 예제의 옵코드들은 해당 C# 메서드를 역어셈블한 코드에서 복사한 것이다. 참조 전달 매개변수에 접근하는 방식이 보통의 매개변수에 접근하는 것과는 다르다는 점을 주목하기 바란다.
    • Lbind와 Stind는 각각 “load indirectly(간접 적재)”와 “store indirectly(간접 저장)”을 뜻한다.
    • 그리고 접미사 R8은 8바이트 부동소수점 수를 뜻한다.
  • out 매개변수의 산출도 이와 동일하다 DefineParameter 호출의 둘째 인수가 다를 뿐이다.
mb.DefineParameter(1, ParameterAttributes.Out, "value");

인스턴스 메서드 생성

  • 인스턴스 메서드를 동적으로 생성할 떄는 DefineMethod 호출시 MethodAttributes.Instance를 지정한다.
ModuleBuilder mb = tb.DefineMethod("SquareRoot", 
  MethodAttributes.Instance | MethodAttributes.Public, 
  ...
  • 인스턴스 메서드에서는 0번 인수가 암묵적으로 this이고 나머지 인수들은 1번부터 시작한다. 따라서 Ldarg_0은 this를 평가 스택에 적자해고, Ldarg_1은 첫 번째 실제 메서드 인수를 적재한다.

메서드 재정의

  • 기반 클래스의 가상 메서드를 재정의하는 것도 어렵지 않다. 그냥 동일한 이름과 서명, 반환 형식의 메서드를 정의하되 DefineMethod 호출시 MethodAttributes.Virtual을 지정하면된다. 인터페이스 메서드를 구현할 때도 마찬가지다.
  • TypeBuilder는 어떤 메서드를 다른 이름으로 재정의하는데 쓰이는 DefineMethodOverride라는 메서드도 제공한다. 그러한 재정의는 명시적 인터페이스 구현에서만 의미가 있다. 그 외의 상황에서는 DefineMethod를 사용해야 한다.

HideBySig 플래그

  • 거의 대부분의 경우, 다른 형식을 파생할 때 MethodAttributes.HideBySig를 지정하는 것이 도움이 된다.
    • HideBySig를 지정하면 C# 스타일의 메서드 숨기기 의미론이 적용된다. 즉, 기반 형식의 메서드는 파생 형식이 그 메서드와 서명이 같은 메서드를 정의할 때만 숨겨진다.
    • HideBySig를 지정하지 않으면 이름만 같아도 메서드가 가려진다. 예컨대 파생 형식의 Foo(string)은 기반 형식의 Foo()를 숨기는데, 대체로 이는 바람직하지 않은 일이다.

필드와 속성의 산출

  • 동적으로 필드를 만들때는 TypeBuilder 객체에 대해 DefineField를 호출한다. 이때 필드의 이름과 형식, 가시성을 지정한다.
    • 다음은 length라는 이름의 전용 정수 필드를 만드는 예이다.
FieldBuilder field = tb.DefineField("length", typeof(int), FieldAttributes.Private);
  • 속성이나 인덱서를 만들려면 추가적인 단계가 필요하다. 우선 TypeBuilder 객체에 대해 DefineProperty를 호출해서 속성의 이름과 형식을 지정한다.
    • 다음은 ‘Text’라는 문자열 속성을 만드는 예이다.
PropertyBuilder prop = tb.DefineField(
  "Text",  // 속성 이름
  PropertyAttributes.None,
  typeof(string),  // 속성 형식
  new Type[0]  // 인덱서 형식들
);
  • 인덱서를 생성할 때는 마지막 인수에 실제 인덱서 형식들의 배열을 지정해야 한다. 이때 속성의 가시성을 지정하지는 않았음을 주목하기 바란다. 가시성은 접근자 메서드들에 대해 개별적으로 지정한다.
  • 다음 단계는 접근자 메서드들, 즉 get 메서드와 set 메서드를 생성하는 것이다.
    • 관례상 이 메서드들의 이름은 “get_”과 “set_”으로 시작한다.
    • 메서드를 생성한 후에는 PropertyBuilder 객체에 대해 SetGetMethod와 SetSetMethod를 호출해서 그 메서드들을 속성에 부착하면 된다.
  • 완전한 예제로 다음과 같은 필드 및 속성 선언을 동적으로 생성하려면
string _text;
publid string Text
{
  get { return _text; }
  internal set { _text = value; }
}
  • 다음과 같이 하면 된다.
FieldBuilder field = tb.DefineField("_text", typeof(string), FieldAttributes.Private);
PropertyBuilder prop = tb.DefineField(
  "Text",  // 속성 이름
  PropertyAttributes.None,
  typeof(string),  // 속성 형식
  new Type[0]  // 인덱서 형식들
);

MethodBuilder getter = tb.DefineMethod(
  "get_Text",  // 메서드 이름
  MethodAttributes.Public | MethodAttributes.SpecialName,
  typeof(string),  // 반환 형식
  new Type[0]);  // 매개변수 형식들

ILGenerator getGen = getter.GetILGenerator();
getGen.Emit(OpCodes.Ldarg_0);  // this를 평가 스택에 적재
getGen.Emit(OpCodes.Ldfld, field);  // 필드 값을 평가 스택에 적재
getGen.Emit(OpCodes.Ret);  // 반환

MethodBuilder setter = tb.DefineMethod(
  "set_Text",  // 메서드 이름
  MethodAttributes.Assembly | MethodAttributes.SpecialName,
  null,  // 반환 형식
  new Type[] { typeof (string) });  // 매개변수 형식들

ILGenerator setGen = getter.GetILGenerator();
setGen.Emit(OpCodes.Ldarg_0);  // this를 평가 스택에 적재
setGen.Emit(OpCodes.Ldarg_1);  // 둘째 인수, 즉 속성의 값을 적재
setGen.Emit(OpCodes.Stfld, field);  // 그 값을 필드에 저장
setGen.Emit(OpCodes.Ret);  // 반환

prop.SetGetMethod(getter);  // get 메서드를 속성에 부착
prop.SetSetMethod(setter);  // set 메서드를 속성에 부착
  • 이제 속성을 시험해 보자.
Type t = tb.CreateType();
object o = Activator.CreateInstance(t);
t.GetProperty("Text").SetValue(o, "산출 성공!", new object[0]);
string text = (string) t.GetProperty("Text").GetValue(o, null);

Console.WriteLine(text);  // 산출 성공!
  • 접근자 MethodAttributes를 정의할 때 SpecialName 플래그를 포함했음을 주목하기 바란다. 이렇게 하면 어셈블리를 정적으로 참조할 때 컴파일러가 이 메서드들을 직접 바인딩하지 않는다. 또한 이 플래그를 지정하면 반영 도구들과 Visual Studio의 IntelliSense가 접근자들을 적절히 처리한다.
  • 이벤트도 이와 비슷한 방식으로 생성할 수 있다. TypeBuilder에 대해 DefineEvent를 호출하고, 명시적인 이벤트 접근자 메서드들을 생성하고, EventBuilder에 대해 SetAddOnMethod와 SetRemoveOnMethod를 호출해서 그 메서드들을 이벤트에 부착하면 된다.

생성자의 산출

  • 생성자를 동적으로 생성할 떄에는 TypeBuilder에 대해 DefineConstructor를 호출한다.
    • 그런데 꼭 생성자를 만들어야 하는 것은 아니다. 생성자를 만들지 않으면 매개변수 없는 기본 생성자가 자동으로 제공된다.
    • C#에서처럼 파생 형식의 경우 기본 생성자는 기반 클래스의 생성자를 호출한다.
    • 하나 이상의 생성자를 직접 생성하는 이 기본 생성자는 제거된다.
  • 필드들을 초기화하는 장소로는 생성자가 제격이다.
    • 사실 동적 코드 생성의 경우 생성자가 유일한 장소이다. C# 필드 초기지 절에 해당하는 CLR의 동적 코드 생성 요소가 없기 때문이다.
    • 사실 C#의 필드 초기치 절은 생성자 안에서 필드 값을 배정하는 코드의 단축 표기 수단일 뿐이다.
  • 예컨대 다음과 같은 클래스르 생각해 보자.
class Widget
{
  int _capacity = 4000;
}
  • 다음은 이 클래스의 필드 초기치 절과 같은 일을 하는 생성자를 동적으로 생성하는 예이다.
FieldBuilder field = tb.DefineField("_capacity", typeof(int), FieldAttributes.Private);
ConstructorBuilder c = tb.DefineConstructor(
  MethodAttributes.Public,
  CallingConventions.Standard,
  new Type[0]);  // 생성자 매개변수 형식들

ILGenerator gen = c.GetILGenerator();

gen.Emit(OpCodes.Ldarg_0);  // this를 평가 스택에 적재
gen.Emit(OpCodes.Ldc_I4, 4000);  // 값 4000을 평가 스택에 적재
gen.Emit(OpCodes.Stfld, field);  // 그 값을 필드에 저장
gen.Emit(OpCodes.Ret);

기반 생성자 호출

  • 다른 형식을 파생해서 형식을 만들 떄 앞의 예제처럼 생성자를 만들면 기반 클래스의 생성자가 호출되지 않는다.
    • 이는 기반 클래스 생성자가 간접적으로든 직접적으로든 항상 호출되는 C#과는 다른 방식이다.
    • 예컨대 다음과 같은 클래스들이 있다고 하자.
class A { public A() { Console.Write("A"); } }
class B : A { public B() { } }
  • 컴파일러는 둘째 줄을 사실상 다음과 같이 바꾸어서 컴파일 한다.
class B : A { public B() : base() { } }
  • 그러나 IL 코드를 동적으로 생성할 때는 자도응로 이런 처리가 일어나지 않으므로 기반 클래스 생성자를 실행하고 싶으면(거의 항상 그래야 한ㄷ) 반드시 기반 클래스 생성자를 명시적으로 호출해야 한다.
    • 다음은 A라는 기반 클래스의 생성자를 호출하는 코드를 생성하는 예이다.
gen.Emit(OpCodes.Ldarg_0);
ConstructorInfo baseConstr = typeof(A).GetCOnstructor(new Type[0]);
gen.Emit(OpCodes.Call, baseConstr);
  • 인수들을 지정해서 생성자를 호출할 수도 있는데, 인수들을 지정하는 방법은 메서드를 호출할 때와 동일하다.

특성 부착

  • 동적 코드 요소에 커스텀 특성을 부착할 때는 CustomAttributeBuilder 객체에 대해 SetCustomAttribute를 호출한다.
    • 예컨대 다음과 같은 특성 선언을 필드나 속성에 부착한다고 하자.
[XmlElement("FirstName", Namespace="http://test/", Order=3)]
  • 이 특성 선언은 문자열 하나를 받는 XmlElementAttribute 생성자를 호출하는 것에 해당한다.
    • CustomAttributeBuilder를 사용하려면 우선 이 생성자에 관한 객체를 얻어야 하며, 설정하고자 하는 두 속성(Namespace와 Order)에 관한 객체들도 얻어야 한다.
Type attType = typeof(XmlElementAttribute);
ConstructorBuilder attConstructor = attType.GetConstructor(new Type[] { typeof(string) });

var att = new CustomAttributeBuilder(
  attConstructor,  // 생성자
  new object[] { "FirstName" },  // 생성자 인수들
  new PropertyInfo[]
  {
    attType.GetProperty("Namespace"),  // 속성
    attType.GetProperty("Order")
  },
  new object[] { "http://test/", 3 }  // 속성 값들
};

myFieldBuilder.SetCustomAttribute(att);
// 또는 propBuilder.SetCustomAttribute(att);
// 또는 typeBuilder.SetCustomAttribute(att); 등등

제네릭 메서드와 제네릭 형식의 산출

  • 이번 절의 모든 예제는 modeBuilder가 다음과 같이 인스턴스화 되어 있다고 가정한다.
AppDomain appDomain = AppDomain.CurrentDomain;
AssemblyName aname = new AssemblyName("MyEmissions");

AssemblyBiulder assemBuilder = domain.DefineDynamicAssembly(aname, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule("MainModule", "MyEmissions.dll");

제네릭 메서드의 생성

  • 제네릭 메서드를 생성하는 과정은 다음과 같다.
    1. MethodBuilder 객체에 대해 DefineGenericParameters를 호출해서 GenericTypeParameterBuilder 객체들의 배열을 얻는다.
    2. MethodBuilder에 대해 SetSignature를 호출한다. 이때 단계 1에서 얻은 제네릭 형식 매개변수들의 배열을 지정한다.
    3. 필요하다면 형식 매개변수들의 이름을 명시적으로 설정한다.
  • 예컨대 다음과 같은 제네릭 메서드를 생각해 보자.
public static T Echo<T> (T value)
{
  return value;
}
  • 다음은 이를 동적으로 생성하는 코드이다.
TypeBuilder tb = modBuilder.DefineType("Widget", TypeAttributes.Public);
MethodBuilder mb = tb.DefineMethod("Echo", MethodAttributes.Public | MethodAttributes.Static);

GenericTypeParameterBuilder[] genericParams = mb.DefineGenericParameters("T");

mb.SetSignature(genericParams[0]  // 반환 형식
  null, null
  enericParams,  // 매개변수 형식들
  null, null);

mb.DefineParameter(1, ParameterAttributes.None, "value");  // 생략 가능

ILGenerator gen = mb.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ret);
  • DefineGenericParameters 메서드는 임의의 개수의 문자열 인수들을 받는다. 이 문자열들은 원하는 제네릭 형식 매개변수 이름들에 대응된다.
    • 지금 예제는 T라는 제네릭 형식 매개변수 하나만 정의한다. GenericTypeParameterBuilder는 System.Type을 상속하므로, 옵코드 산출시 TypeBuilder 대신 사용할 수 있다.
  • GenericTypeParameterBuilder에는 기반 형식 제약 조건을 설정하는 메서드도 있다.
genericParams[0].SetBaseTypeConstraint(typeof(Foo));
  • 또한 다음처럼 인터페이스 제약 조건을 설정할 수도 있다.
genericParams[0].SetInterfaceConstraints(typeof(IComparable));
  • 예컨대 아래의 제약 조건을 동적으로 지정하려면
publci static T Echo<T> (T value) where T : IComparable<T>
  • 다음과 같이 하면 된다.
genericParams[0].SetInterfaceConstraints(typeof(IComparable<>).MakeGenericType(genericParams[0]));
  • 다른 종류의 제약 조건들은 SetGenericParameterAttributes 메서드로 지정하면 된다. 이 메서드는 다음과 같은 값들이 있는 GenericParameterAttributes 열거형의 한 값을 받는다.
DefulatConstructorConstraint
NotNullableValueTypeConstraint
ReferenceTypeConstraint
Covariant
Contravariant
  • 마지막 두 멤버는 형식 매개변수에 out과 int 수정자를 적용하는 것에 해당한다.

제네릭 형식의 생성

  • 제네릭 형식도 제네릭 메서드와 비슷한 방식으로 생성할 수 있다. 차이점은 MethodBuilder가 아니라 TypeBuilder에 대해 DefineGenericParameters를 호출한다는 점이다.
    • 예컨대 다음과 같은 제네릭 클래스를 동적으로 생성하려면
public class Widget<T>
{
  public T Value;
}
  • 다음과 같이 하면 된다.
TypeBuilder tb = modBuilder.DefineType("Widget", TypeAttributes.Public);
GenericTypeParameterBuilder[] genericParams = tb.DefineGenericParameters("T");
tb.DefineField("Value", genericParams[0], FieldAttributes.Public);
  • 제네릭 제약 조건들을 추가하는 방법은 메서드에서와 같다.

까다로운 산출 대상들

미생성된 형식과 닫힌 제네릭 형식

  • 다음처럼 닫힌 제네릭 형식을 사용하는 메서드를 동적으로 생성한다고 하자.
public class Widget
{
  public static void Test() { var list = new List<int>(); }
}
  • 생성 과정은 다음과 같이 간단하다.
TypeBuilder tb = modBuilder.DefineType("Widget", TypeAttributes.Public);
MethodBuilder mb = tb.DefineMethod("Test", MethodAttributes.Public | MethodAttributes.Static);
ILGenerator gen = mb.GetILGenerator();
Type variableType = typeof(List<int>);

ConstructorInfo ci = variableType.GetConstructor(new Type[0]);

LocalBuilder listVar = gen.DeclareLocal(variableType);
gen.Emit(OpCodes.Newobj, ci);
gen.Emit(OpCodes.Stloc, listVar);
gen.Emit(OpCodes.Ret);
  • 이번에는 메서드가 정수들의 목록이 아니라 위젯들의 목록을 사용한다고 하자.
public class Widget
{
  public static void Test() { var list = new List<Widget>(); }
}
  • 언뜻 보기에는 간단한 수정으로 해결될 것 같다. 즉 다으로 수정하면 끝날 것이다.
Type variableType = typeof(List<int>).MakeGenericType(tb);
  • 안타깝게도 이렇게 하면 GetConstructor 호출시 NotSupportedException 예외가 발생한다. 미생성된, 즉 아직 CreateType을 호출하지 않은 형식 구축 객체(지금 예제의 TypeBuilder 객체)로 닫은 제네릭 형식에 대해 GetConstructor를 호출하면 안되기 때문이다.
    • GetField 메서드나 GetMethod 메서드도 마찬가지이다.
  • 해결책은 그리 직관적이지 않다. TypeBuilder는 다음과 같은 세 정적 메서드를 제공한다.
public static ConstructorInfo GetConstructor(Type, ConstructorInfo);
public static FieldInfo GetField(Type, FieldInfo);
public static MethodInfo GetMethod(Type, MethodInfo);
  • 메서드 이름이나 서명만 보고는 잘 모르겠지만, 사실 이 메서드들의 주된 용도는 미생성된 형식 구축 객체로 닫은 제네릭 형식의 멤버를 얻는 것이다.
    • 첫 매개변수는 닫힌 제네릭 형식이고, 둘째 매개변수는 얻고자 하는 묶이지 않은 제네릭 형식의 멤버이다.
    • 정리하자면 List<Widget>을 사용하는 메서드를 동적으로 생성하려면 다음과 같이 해야 한다.
TypeBuilder tb = modBuilder.DefineType("Widget", TypeAttributes.Public);
MethodBuilder mb = tb.DefineMethod("Test", MethodAttributes.Public | MethodAttributes.Static);
ILGenerator gen = mb.GetILGenerator();
Type variableType = typeof(List<>).MakeGenericType(tb);

ConstructorInfo unbound = typeof(List<>).GetConstructor(new Type[0]);
ConstructorInfo ci = TypeBuilder.GetConstructor(variableType, unbound);

LocalBuilder listVar = gen.DeclareLocal(variableType);
gen.Emit(OpCodes.Newobj, ci);
gen.Emit(OpCodes.Stloc, listVar);
gen.Emit(OpCodes.Ret);

순환 참조

  • 서로를 참조하는 두 형식을 동적으로 구축한다고 하자. 예컨대 다음 두 클래스를 생각해 보자.
class A { public B Bee; }
class B { public A Aye; }
  • 다음은 이들을 동적으로 생성하는 예이다.
var publicAtt = FieldAttributes.Public;

TypeBiulder aBuilder = modBuilder.DefineType("A");
TypeBiulder bBuilder = modBuilder.DefineType("B");

FieldBuilder bee = aBuilder.DefineField("Bee", bBuilder, publicAtt);
FieldBuilder aye = bBuilder.DefineField("Aye", aBuilder, publicAtt);

Type realA = aBuilder.CreateType();
Type realB = bBuilder.CreateType();
  • aBuilder와 bBuilder를 모두 채운 후에야 둘에 대해 CreateType을 호출했음을 주목하기 바란다. 원칙은 일단 모든 것을 연결하고 그런 다음 각 형식 구축 객체에 CreateType을 호출한다는 것이다.
  • 흥미롭게도 realA 형식은 bBuilder에 대해 CreateType을 호출하기 전까지는 유요하지만 고장 난(dysfunctional) 상태라는 점이다. (bBuilder에 대해 CreateType을 호출하기 전에 aBuilder를 사용하기 시작하면, Bee 필드에 접근할 때 예외가 발생한다.)
  • realB를 생성한 후 bBuilder는 realA를 어떻게 고치는 것일까? 답은 고치지 않는다는 것이다. realA는 다음번에 쓰일 때 스스로 자신을 고친다.
    • 이것이 가능한 이유는 CreateType을 호출하고 나면 TypeBuilder가 실제 실행시점 형식에 대한 하나의 프록시로 변하기 때문이다.
    • 따라서 bBuilder에 대한 참조를 가진 realA는 업그레이드에 필요한 메타자료를 손쉽게 구할 수 있다.
  • 이러한 시스템은 형식 구축 객체가 미생성 형식에 대해 간단히 정보를 요구할 떄 다시 말하면 형식이나 멤버, 객체 참조같이 미리 결정할 수 있는 정보를 요구할 떄 유효하다.
    • 예컨대 realA 생성시 형식 구축 객체는 realB가 메모리에서 몇 바이트의 공간을 차지하게 될지 알 필요가 없다. 어차피 realB는 아직 생성되지 않았으므로 그런 정보는 아직 몰라도 된다.
    • 그러나 realB가 구조체이면 이야기가 달라진다. 그런 경우 realB의 최종 크기는 realA 생성에 꼭 필요한 정보이다.
  • 관계가 비순환적이면 문제가 간단하다. 다음과 같은 관계의 구조체들을 동적으로 만든다면
struct A { public B Bee; }
struct B {  }
  • 그냥 먼저 구조체 B를 생성한 후 A를 생성하면 그만이다. 그러나 다음과 같은 관계는 어떨까?
struct A { public B Bee; }
struct B { public A Aye;  }
  • 두 구조체가 서로를 담고 있다는 것 자체가 모순이므로, 이런 코드의 동적 생성은 시도할 필요가 없다(정적인 경우도 마찬가지다. C# 컴파일러는 이런 코드에 대해 오류를 발생한다). 하지만 다음과 같은 변형은 유효하고 유용하다.
public struct S<T> { ... }  // S가 빈 구조체여도 이 예제는 작동한다.

class A { S<B> Bee; }
class B { S<A> Aye;  }
  • A를 생성할 때 TypeBuilder는 B의 메모리 사용량을 알 필요가 있고, 그 역도 마찬가지다.
    • 이해를 돕기 위해 구체적인 예를 보자. 구조체 S는 정적으로 정의되어 있다고 하겠다. 다음은 클래스 A와 B를 동적으로 생성하는 코드이다.
var pub = FieldAttributes.Public;

TypeBiulder aBuilder = modBuilder.DefineType("A");
TypeBiulder bBuilder = modBuilder.DefineType("B");

aBuilder.DefineField("Bee", typeof(S<>).MakeGenericType(bBuilder), pub);
bBuilder.DefineField("Aye", typeof(S<>).MakeGenericType(aBuilder), pub);

Type realA = aBuilder.CreateType();  // 오류: 형식 B를 적재할 수 없음.
Type realB = bBuilder.CreateType();
  • 안타깝게도 이번에는 CreateType이 TypeLoadException 예외를 던진다. CreateType 호출 순서를 어떻게 바꾸든 마찬가지다.
  • 형식 있는 LINQ to SQL DataContext를 동적으로 산출할 때 이런 문제에 봉착할 수 있다. 제네릭 EntityRef 형식은 하나의 구조체로 지금 예제의 S에 해당한다. 데이터베이스의 두 테이블이 다른 어떤 상호 부모/자식 관계를 통해서 연결되면 순환 참조가 발생한다.
  • 이 문제를 해결하려면 형식 구축 객체가 realA 생성을 통해서 realB를 부분적으로 생성할 수 있게 해야 한다.
    • 구체적으로 말하면 CreateTYpe 호출 직전에 현재 응용 프로그램 도메인의 TypeResolve 이벤트를 처리해야 한다. 지금 예제에서는 마지막 두 줄을 다음으로 대체하면 된다.
TypeBuilder[] uncreatedTypes = { aBuilder, bBuilder };

ResolveEnventHandler handler = delegate(object o, ResolveEventArgs args)
{
  var type = uncreatedTypes.FirstOrDefault(t => t.FullName == args.Name);
  return type == null ? null : type.CreateType.Assembly;
}

AppDomain.CurrentDomain.TypeResolve += handler;

Type realA = aBuilder.CreateType();
Type realB = bBuilder.CreateType();

AppDomain.CurrentDomain.TypeResolve -= handler;
  • 중첩된 형식과 부모 형식이 서로 참조하는 형태의 중첩된 형식을 동적으로 정의할 때에도 순환 참조 문제가 발생하는데, 역시 이 예제에서처럼 TypeResolve 이벤트를 처리해서 해결해야 한다.

IL 코드의 파싱

  • 기존 메서드의 내용에 대한 정보를 얻고자 할 때는 우선 MethodBase 객체에 대해 GetMethodBody를 호출한다.
    • 이 메서드는 MethodBody 객체를 돌려주는데, 그 객체에는 메서드의 지역 변수들, 예외 처리 블록들, 스택 크기를 알 수 있는 속성들이 있으며, 실제 IL 코드를 담은 속성도 있다.
    • 이런 메서드들과 객체들을 사용하는 것은 Reflection.Emit의 형식들로 하는 일의 반대에 해당한다.
  • 이러한 메서드도 IL 코드 조사는 코드의 프로파일링에 유용하다. 그러한 프로파일링의 간단한 용도 하나는, 어셈블리를 갱신했을 때 어셈블리의 어떤 메서드가 변했는지 파악하는 것이다.
  • 반영 API에서 C#의 모든 기능적 코드 요소는 MethodBase의 한 파생 형식 또는 MethodBase 객체가 부착된 어떤 형식(속성, 이벤트, 인덱서의 경우)으로 대표된다는 점을 기억하기 바란다.

역어셈블러 작성

  • 이 예제의 소스 코드를 http://www.albahari.com/nutshell/에서 내려받을 수 있다.
  • 다음은 예제 역어셈블러의 출력 예이다.
IL_00EB:  ldfld  Disassembler._pos
IL_00F0:  ldoc.2
IL_00F1:  add
IL_00F2:  ldelema  System.Byte
IL_00F7:  ldstr  "Hello world"
IL_00FC:  call  System.Byte.ToString
IL_0101:  ldstr  " "
IL_0106:  call  System.String.Concat
  • 이러한 출력을 얻으려면 IL 코드를 구성하는 이진 토큰들을 파싱해야 한다. 가장 먼저 할 일은 MethodBody에 대해 GetILAsByteArray 메서드를 호출해서 바이트 배열 형태의 IL 코드를 얻는 것이다.
    • 이후의 작업을 편하게 진행하기 위해 이를 하나의 클래스로 작성해 두자.
public class Disassembler
{
  public static string Disassemble (MethodBase method) => new Disassembler (method).Dis();

  StringBuilder _output;  // 여기에 출력 결과를 계속 추가한다.
  Module _module;  // 나중에 요긴하게 사용할 것이다.
  byte[] _il;  // 실제 바이트 코드
  int _pos;  // 바이트 코드 안에서의 현재 위치

  Disassembler(MethodBase method)
  {
    _module = method.DeclaringType.Module;
    _il = method.GetMethodBody().GetILAsByteArray();
  }

  string Dis()
  {
    _output = new StringBuilder();
    while (_pos < _il.Length)
      DisassembleNextInstruction();
    return _output.ToString();
  }
}
  • 정적 Disassemble 메서드는 이 클래스의 유일한 공용 멤버이다. 다른 모든 멤버는 역어셈블 과정에서 이 클래스 내부에서만 사용하는 전용 멤버이다. Dis 메서드는 각 명령을 처리하는 ‘주’ 루프를 담고 있다.
  • 이러한 뼈대를 갖추었다면, 이제 남은 일은 DisassembleNextInstruction를 실제로 구현하는 것이다.
    • 그전에 IL의 옵코드를 8비트 또는 16비트 옵코드 값으로 조회할 수 있도록 모든 옵코드를 하나의 정적 사전에 적재해 두면 편할 것이다.
    • 그런 사전을 마련하는 가장 쉬운 방법은 OpCodes 클래스에서 형식이 OpCode인 모든 정적 필드를 반영 긴으을 이용해서 조회하는 것이다.
static Dictionary<short, OpCode> _opcodes = new Dictionary<short, OpCode>();

static Disassembler()
{
  Dictionary<short, OpCode> opcodes = new Dictionary<short, OpCode>();
  foreach (FieldInfo fi in typeof(OpCodes).GetFields(BindingFlags.Public | BindingFlags.Static))
  {
    if (typeof (OpCode).IsAssignableFrom(fi.FieldType))
    {
      OpCode code = (OpCode) fi.GetValue(null);  // 필드의 값을 얻는다.
      if (code.OpCodeType != OpCodeType.Nternal)
        _opcodes.Add(code.Value, code);
    }
  }
}
  • 정적 생성자에서 사전을 채우므로 사전은 단 한 번만 채워진다.
  • 이제 DisassembleNextInstruction을 실제로 작성해 보자. 하나의 IL 명령은 1바이트 또는 2바이트 옵코드 하나와 0, 1, 2, 4, 8바이트의 피연산자(operand; 연산대상) 하나로 이루어진다. (예외는 인라인 switch 문 옵코드들인데, 그런 옵코드 다음에는 가변 개수의 피연산자들이 온다)
    • 따라서 바이트 코드에서 옵코드를 읽고 그 다음에 피연산자를 읽고 그것들을 적절한 서식으로 출력하면 된다.
void DisassembleNextInstruction()
{
  int opStart = _pos;
  OpCode code = ReadOpCode();
  string operand = ReadOperand(code);
  _output.AppendFormat("IL_{0:X4}: {1,-12} {2}", opStart, code.Name, operand);
  _output.AppendLine();
}
  • 옵코드를 읽는 메서드는 우선 바이트 하나를 읽어서 유효한 옵코드인지 점검한다. 만일 유효한 옵코드가 아니면 바이트 하나를 더 읽어서 유효한 2바이트 옵코드인지 점검한다.
OpCode ReadOpCode()
{
  byte byteCode = _il[_pos++];

  if (_opcodes.ContainsKey(byteCOde))
    return _opcodes[byteCode];

  if (_pos == _il.Length)
    throw new Exception("IL이 예기치 않게 끝났음");

  short shortCode = (short)(byteCode * 256 + _il[_pos++]);

  if (!_opcodes.ContainsKey(shortCode))
    throw new Exception ("다음 옵코드를 찾을 수 없음: " + shortCode);

  return _opcodes[shortCode];
}
  • 피연산자를 읽는 메서드에서는 먼저 피연산자의 형식에 근거해서 그 길이를 파악한다. 대부분의 피연산자 형식은 4바이트이므로, 4바이트가 아닌 경우들만 조건절을 이용해서 걸러내면 간단하게 해결된다.
  • 그런 다음에는 FormatOperand를 호출해서 피연산자를 적절한 형식으로 서식화 한다.
string ReadOperand(OpCode c)
{
  int operlandLength = c.OperandType == OperandType.InlineNone
    ? 0 : 
    c.OperandType == OperandType.ShortInlineBrTarget || 
    c.OperandType == OperandType.ShortInlineI || 
    c.OperandType == OperandType.ShortInlineVar
    ? 1 :  
    c.OperandType == OperandType.InlineVar
    ? 2 :  
    c.OperandType == OperandType.InlineI8 ||
    c.OperandType == OperandType.InlineR
    ? 8 :  
    c.OperandType == OperandType.InlineSwitch
    ? 4 * (BitConverter.ToInt32(_il, _pos) + 1) :
    4;  // 그 외는 모두 4바이트

  if (_pos + operandLength > _il.Length)
    throw new Exception ("Unexpected end of IL");

  string result = FormatOperand(c, operandLength);
  if (result == null)
  {  // 피연산자 바이트들을 십육진수로 출력
    result = "";
    for (int i = 0 i < operandLength; i++)
      return += _il[_pos + i].ToString("X2") + " " ;
  }
  _pos += operandLength;
  return result;
}
  • FormatOperand가 돌려준 result가 null이면 특별한 서식화가 필요 없다는 뜻이므로 그냥 피연산자 바이트들을 십육진수 형태로 출력하면 된다.
    • 이 시점에서 FormatOperand 메서드를 그냥 항상 null을 돌려주도록 간단하게 작성해서 역어셈블러를 시험해 볼 수 있다.
    • 다음은 그러한 FormatOperand 메서드를 이용한 역어셈블 결과의 예이다.
IL_00A8:  ldfld  98 00 00 04
IL_00AD:  ldoc.2
IL_00AE:  add
IL_00AF:  ldelema  64 00 00 01
IL_00B4:  ldstr  26 04 00 70
IL_00B9:  call  B6 00 00 0A
IL_00BE:  ldstr  11 01 00 70
IL_00C3:  call  91 00 00 0A
...
  • 출력을 보면 옵코드들이 정확함을 확인할 수 있다. 그러나 피연산자는 별로 도움이 되지 않는다. 십육진수 대신 멤버 이름과 문자열이 표시되면 훨씬 좋을 것이다.
    • 이를 위해 피연산자의 형식에 따라 적절한 서식화를 가하도록 FormatOperand 메서드를 제대로 작성해 보자.
    • 대부분의 4바이트 피연산자와 짧은 분기 명령들에 그러한 서식화가 필요하다.
string FormatOperand(OpCode c, int operandLength)
{
  if (operandeLenght == 0) 
    return "";

  if (operandeLenght == 4) 
    return Get4ByteOperand(c);
  else if (c.OperandtType == OperandType.ShortInlineBrTarget)
    return GetShortRelativeTarget();
  else if (c.OperandType == OperandType.InlineSwitch)
    return GetSwitchTarget(operandeLength);
  else 
    return null;
}
  • 특별한 취급이 필요한 4바이트 피연산자는 세 종류이다.
    • 첫째는 멤버나 형식에 대한 참조인데, 이에 대해서는 현재 메서드를 정의하고 있는 모듈의 ResolveMember 메서드를 호출해서 멤버 이름이나 형식 이름을 얻는다.
    • 둘째는 문자열이다. 문자열 피연산자는 어셈블리 모듈의 메타자료에 들어 있으며, ResolveString 메서드를 호출해서 조회할 수 있다.
    • 마지막은 분기 대상으로 이 경우 피연산자는 IL 코드 안에서의 바이트 오프셋이다. 이 피연산자는 이른 현재 명령 다음 위치에 해당하는 지점(+4바이트)의 절대 주소로 바꾸어서 서식화 한다.
string Get4ByteOperand(OpCode c)
{
  int intOp = BitConverter.ToInt32(_il, _pos);

  switch (c.OperandType)
  {
    case OperandType.InlineTok:
    case OperandType.InlineMethod:
    case OperandType.InlineField:
    case OperandType.InlineType:
      MemberInfo mi;
      try { mi = _module.ResolveMember(intOp); }
      catch { return null; }
      if (mi == null) return null;

      if (mi.ReflectedType != null)
        return mi.ReflectedType.FullName + "." + mi.Name;
      else if (mi is Type)
        return ((Type)mi).FullName;
      else 
        return mi.Name;

    case OperandType.InlineString:
      string s = _module.ResolveString(intOp);
      if (s != null) s = "'" + s + "'";
      return s;

    case OperandType.InlineBrTarget:
      return "IL_" + (_pos + intOp + 4).ToString("X4");

    defulat:
      return null;
  }
}
  • 코드 분석 도구를 만들고 싶다면, 이 메서드에서 ResolveMember를 호출하는 지점이 메서드 의존성들을 파악하기에 좋은 장소임을 기억하리 바란다.
  • 그 외의 4바이트 옵코드에 대해서는 그냥 null을 돌려준다(그러면 ReadOperand는 피연산자를 십육진수로 서식화한다)
  • 이러한 4바이트 옵코드들 외에 짧은 분기 대상과 인라인 switch 문의 피연산자도 특별한 취급이 필요하다.
    • 짧은 분기 대상의 피연산자는 부호 있는 바이트 하나로 현재 명령 다음 위치(즉, +1바이트)를 기준으로 한 오프셋에 해당한다.
    • switch 문 피연산자는 가변 개수의 4바이트 분기 대상들이다.
string GetShortRelativeTarget()
{
  int absoluteTaget = _pos + (sbyte) _il[_pos] + 1;
  return "IL_" + absoluteTarget.ToString("X4");
}

string GetSwitchTarget(int operandLength)
{
  int tagetCount = BitConverter.ToInt32(_il, _pos);
  string[] targets = new string[targetCount];
  for (int i = 0; i < targetCount; i++)
  {
    int ilTarget = BitConverter.ToInt32(_il, _pos + (i+1) * 4);
    targets[i] = "IL_" + (_pos + iltarget + operandLength).ToString("X");
  }
  return "(" + string.Join(", ", targets) + ")";
}
  • 이렇게 해서 역어셈블러가 완성되었다. 다음은 역어셈블러 자신의 한 메서드를 역어셈블하는 예이다.
MethodInfo mi = typeof(Disassembler).GetMethod("ReadOperand", BindingFlags.Instance | BindingFlags.NonPublic);
Console.WriteLine(Disassembler.Disassemble(mi));

 

[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.