C# 6.0 완벽 가이드/ 상호운용성

네이티브 DLL 호출

  • Platform Invocation Services(플랫폼 호출 서비스)를 줄인 P/Invoke는 .NET 응용 프로그램에서 비관리(unmanaged; .NET이 관리하지 않는) DLL에 있는 함수나 구조체, 콜백에 접근하는데 사용하는 기술이다.
  • 예컨대 Windows DLL user32.dll에 있는 MessageBox 함수를 생각해 보자. 이 C함수는 다음과 같이 선언되어 있다.
int MessageBox(HWND hWnd, LPCTSTR lpText, LPCTStr lpCaption, UINT uType);
  • .NET 응용 프로그램에서 이 함수를 직접 호출하는 것은 생각보다 쉽다. 같은 이름의 정적 메서드를 선언하되 extern 키워드를 적용하고 DllImport 특성을 부여하면 된다.
using System;
using System.Runtime.IneropServices;

class MsgBoxTest
{
  [DllImport("user32.dll")]
  static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);

  public static void Main()
  {
    MessageBox(IntPtr.Zero, "Please do not press this again.", "Attention", 0);
  }
}
  • 실제로 System.Windows 이름공간과 System.Windows.Forms 이름공간에 있는 MessageBox 클래스들이 이와 비슷한 비관리 메서드들을 이런 식으로 호출한다.
  • CLR에는 .NET 형식들과 비관리 형식들 사이에서 매개변수들과 반환 값들을 변환하는 방법을 아는 인도기(marshaler)가 있다.
    • 지금 예에서는 int 매개변수는 함수가 기대하는 4바이트 정수로 직접 대응되며, 문자열 매개변수는 2바이트 유니코드 문자들의 널 종료(null-terminated) 배열로 변환된다.
    • IntPtr은 비관리 핸들을 캡슐화 하도록 만들어진 하나의 구조체로, 그 너비는 32비트 플랫폼에서는 32비트이고 64비트 플랫폼에서는 64비트이다.

형식 인도

공통 형식의 인도

  • 비관리 코드에서는 주어진 한 종류의 자료를 나타내는 형식이 여러 개일 수 있다.
    • 한 예로 문자열은 단일 바이트 ANSI 문자들의 배열일 수도 있고 2바이트 유니코드 문자들의 배열일 수 있으며, 널로 끝나는 가변 길이 배열일 수도 있고 길이가 고정된 배열일 수도 있다.
    • 인도기가 어떤 것을 사용할 것인지를 명시적으로 지정하려면 다음 예처럼 [MarshalAs] 특성을 지정하면 된다.
[DllImport("...")]
static extern int Foo([MarshalAs(UnmanagedType.LPStr)] string s);
  • UnmanagedType 열거형에는 인도기가 이해하는 Win32와 COM의 모든 형식에 해당하는 멤버들이 있다.
    • 지금 예는 인도기에게 문자열 LPStr에 대응시키라고 지정한다. LPStr은 널 종료(널 문자로 끝나는) 단일 바이트 ANSI 문자열이다.
  • .NET 쪽에서도 여러 형식 중 하나를 선택해야 할 수 있다. 예컨대 비관리 핸들을 IntPtr에 대응시킬 수도 있고, int나 uint, long, ulong에 대응시킬 수도 있다.
  • 대부분의 비관리 핸들은 메모리 주소나 포인터를 캡슐화하므로, 32비트 운영체제와 64비트 운영체제 모두에서 호환되게 하려면 반드시 IntPtr에 대응시켜야 한다. 그러한 핸들의 전형적인 예는 HWND이다.
  • Win32 함수들에서는 WinUser.h 같은 C++ 헤더 파일에 정의되어 있는 일단의 상수 중 하나를 받는 정수 매개변수를 흔히 볼 수 있다.
    • 그런 상수들을 개별 C# 상수들로 정의하는 것보다는 하나의 열거형으로 묶는 것이 바람직하다. 열거형을 사용하면 코드가 깔끔해지고 정적 형식 안전성도 높아지기 때문이다.
  • Visual Studio를 설치할 때, C++의 다른 구성요소들은 설치하지 않더라도 C++ 헤더 파일들은 꼭 설치하기 바란다. 모든 네티이브 Win32 상수들이 거기에 정의되어 있다.
  • 비관리 코드에서 .NET 코드로 문자열을 인도하는 과정에서 어느 정도의 메모리 관리가 일어난다.
    • 만일 외부 메서드를 선언할 때 문자열을 string이 아니라 StringBuilder로 선언하면 그런 작업을 CLR의 인도기가 자동으로 처리해 준다.
    • 다음이 그러한 예이다.
using System;
using System.Text;
using System.Runtime.IneropServices;

class Test
{
  [DllImport("kernel32.dll")]
  static extern int GetWindowsDirectory(StringBuilder sb, int maxChars);

  public static void Main()
  {
    StringBuilder s = new StringBuider(256);
    GetWindowsDirectory(s, 256);
    Console.WriteLine(s);
  }
}

클래스와 구조체의 인도

  • 비관리 메서드에 구조체를 넘겨 주어야 할 때가 종종 있다. 예컨대 다음과 같이 정의된 Win32 API 함수 GetSystemTime을 생각해 보자.
void GetSystemTime(LPSYSTEMTIME lpSystemTime);
  • 이 함수가 받는 LPSYSTEMTIME은 다음과 같은 C 구조체를 가리키는 포인터이다.
typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;
  • GetSystemTime을 호출하려면 이 C 구조체와 부합하는 .NET 클래스 또는 구조체를 정의해야 한다.
using System;
using System.Runtime.IneropServices;

[StructLayout(LayoutKind.Sequential)]
class SystemTime
{
  public ushort Year;
  public ushort Month;
  public ushort DayOfWeek;
  public ushort Day;
  public ushort Hour;
  public ushort Minute;
  public ushort Second;
  public ushort Milliseconds;
}
  • [StructLayout] 특성은 이 클래스의 각 필드를 비관리 구조체의 필드들에 대응시키는 방법을 인도기에 알려주는 역할을 한다.
    • LayoutKind.Sequential은 필드들을 팩 크기(pack-size) 경계에 맞게 차례로 정합(alignment)하라는 뜻이다.
    • 이러한 필드 배치는 해당 C 구조체의 필드 배치와 일치한다.
    • 여기서 필드 이름들은 중요하지 않다. 중요한 것은 필드들의 순서이다.
  • 이제 GetSystemTime을 다음과 같이 호출할 수 있다.
[DllImport("kernel32.dll")]
static extern int GetSystemTime(SystemTime t);

public static void Main()
{
  SystemTime t = new SystemTime();
  GetSystemTime(t);
  Console.WriteLine(t.Year);
}
  • C에서나 C#에서나 한 객체의 필드들은 그 객체의 시작 위치에서 n바이트만큼 떨어진 위치에 있다.
    • C와 C#의 차이는 C# 프로그램에서는 실행시점에서 CLR이 그러한 오프셋을 필드 토큰을 이용해서 찾아내지만 C에서는 컴파일 과정에서 필드 이름에 해당하는 오프셋이 계산, 고정된다는 점이다.
    • 예컨대 C에서 wDay는 SystemTime 객체의 시작 주소에 24바이트를 더한 위차에 있는 것을 지칭하는 토큰일 뿐이다.
  • 접근 속도를 위해, 각 필드의 위치를 결정하는 오프셋은 항상 특정 크기(바이트 수)의 정수배로 결정된다. 그 특정 크기가 바로 앞에서 말한 팩 크기이다.
    • 현재 구현에서 기본 팩 크기는 8바이트이므로, sbyte(1바이트) 필드 하나 다음에 long(8바이트) 필드 하나가 있는 구조체는 16바이트를 차지하며, sbyte 다음의 7바이트는 낭비된다.
    • 이러한 낭비를 줄이려면 팩 크기를 더 줄여야한다. 팩 크기는 [StructLayout] 특성의 Pack 속성으로 설정할 수 있다. 필드들은 설정된 팩 크기의 배수에 해당하는 오프셋으로 배치된다.
    • 예컨대 팩 크기를 1로 설정하면 방금 설명한 구조체는 9바이트만 차지한다. 팩 크기로 설정할 수 있는 값은 1, 2, 4, 8, 16이다.
  • [StructLayout] 특성은 또한 필드 오프셋들을 명시적으로 지정하는 수단도 제공한다.

입력/출력 매개변수의 인도

  • 앞의 예에서 SystemTime을 클래스로 구현했다. 클래스 대신 구조체로 구현할 수도 있는데, 그러면 GetSystemTime을 선언할 때 SystemTime 매개변수에 ref나 out을 붙여야 한다.
[DllImport("kernel32.dll")]
static extern int GetSystemTime(out SystemTime t);
  • 대부분의 경우 C#의 방향 있는 매개변수 의미론은 외부 메서드와 같은 방식으로 작동한다.
    • 즉, C#의 값 전달 매개변수는 비관리 함수의 안으로(in) 복사되고, C# ref 매개변수는 함수 안팎으로(in 및 out) 복사되며, C# out 매개변수는 함수 바깥으로(out) 복사된다.
    • 그러나 변환이 특별한 방식으로 일어나는 형식들도 있다. 예컨대 배열 클래스들과 StringBuilder 클래스는 함수 바깥으로 나올 때 복사가 필요하므로 in/out 매개변수이다.
    • 그런데 이러한 행동 방식을 [In] 특성과 [Out] 특성을 이용해서 임의로 변경하는 것이 유용할 때가 종종 있다.
    • 예컨대 함수가 배열을 읽기 전용으로 사용한다면, 다음처럼 [In] 특성을 지정해서 배열이 항상 함수 안으로만 복사될 뿐 밖으로 복사되지는 않음을 인도기에 알려주는 것이 바람직하다.
static extern void Foo([In] int[] array);

비관리 코드의 콜백 호출

  • 프로그래머들이 관리되는 코드와 관리되지 않는 코드 모두에서 해당 언어의 가장 자연스러운 프로그래밍 모형에 따라 코드를 짤 수 있도록, P/Invoke 계층은 양쪽의 관련된 코드 구축 요소들을 최대한 매끄럽게 대응시키기 위해 노력한다.
    • C#에서 C 함수를 호출하는 것뿐만 아니라 C 함수에서 C# 메서드를 호출할 수도 있으므로(함수 포인터를 통해서), P/Invoke 계층은 비관리 함수 포인터를 그에 가장 가까운 C# 구축 요소인 대리자에 대응시킨다.
  • 한 예로 최상위 창 핸들들을 모두 열거하고 싶다면 User32.dll에 있는 다음 함수를 사용하면 된다.
Bool EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);
  • EnumWindows는 각 최상위 핸들을 인수로 해서 주어진 콜백 함수를 호출한다. 단, 콜백 함수가 false를 돌려주면 열거를 종료한다. 다음은 그러한 콜백 함수를 나타내는 WNDENUMPROC 형식의 정의이다.
Bool CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);
  • C#에서 EnumWindows를 사용하려면 이 콜백 형식에 부합하는 대리자를 선언하고 그 대리자의 인스턴스를 외부 메서드에 전달해야 한다.
using System;
using System.Runtime.IneropServices;

class CallbackFun
{
  delegate bool EnumWindowsCallback(IntPtr hWnd, IntPtr lParam);
  [DllImport("user32.dll")]
  static extern int EnumWindows(EnumWindowsCallback hWnd, IntPtr lParam);

  static bool PrintWindow(IntPtr hWnd, IntPtr lParam)
  {
    Console.WriteLine(hWnd.ToInt64());
    return true;
  }

  static void Main() => EnumWindows(PrintWindow, IntPtr.Zero);
}

C 공용체 흉내 내기

  • 한 구조체(struct)의 각 필드에는 그 필드의 자료를 담기에 충분한 공간이 주어진다. int 필드 하나와 char 필드 하나가 있는 구조체를 생각해 보자.
    • int 필드는 오프셋 0에서 시작할 것이며, 적어도 4바이트의 공간이 보장된다. 따라서 char 필드는 적어도 오프셋 4에서 시작한다.
    • 그런데 어떤 이유로 char 필드가 오프셋 2에서 시작한다면, 그 필드에 어떤 값을 배정하면 int 필드의 값이 바뀔 것이다. 이는 엄청난 재앙이지만, 이상하게도 C 언어는 바로 이런 일을 허용하는 특별한 종류의 구조체를 제공한다.
    • 공용체(union)가 바로 그것이다. C#에서는 [LayoutKind.Explicit] 특성과 [FieldOffset] 특성으로 공용체를 흉내낼 수 있다.
  • 공용체가 유용한 상황을 생각해내기 어렵겠지만, 공용체에는 나름의 용도가 있다.
    • 예컨대 외부 신시사이저에서 어떤 음을 연주한다고 하자. Windows Multimedia API는 MIDI 프로토콜을 통해서 외부 음원을 제어하는 다음과 같은 함수를 제공한다.
[DllImport("winmm.dll")]
public static extern uint midiOutShortMsg (IntPtr handle, uint message);
  • 이 함수의 둘째 인수 message는 연주하고자 하는 음표를 서술하는 정수이다. 이 부호 없는 32비트 정수는 원하는 MIDI 채널, 음표, 음의 세기(구체적으로는 타건 속도)를 뜻하는 여러 바이트로 구성된다.
  • 특정 채널, 음표, 세기로 하나의 32비트 정수를 만들려면 그 바이트들을 비트 단위 <<, >>, &, | 연산자를 이용해서 하나의 ‘압축된’ 32비트 메시지로 조합해야 하는데, 그리 쉬운 일은 아니다.
    • 다행히 그보다 훨씬 쉬운 방법이 있다. 바로 그런 바이트들을 하나의 구조체 형태로 배치하는 것이다.
[StructLayout(LayoutKind.Explicit)]
public struct NoteMessage
{
  [FieldOffset(0)] public uint PackedMsg;  // 전체 길이는 4바이트

  [FieldOffset(0)] public byte Channel;  // FieldOffset이 0임을 주목
  [FieldOffset(1)] public byte Note;
  [FieldOffset(2)] public byte Velocity;
}
  • Channel, Note, Velocity 필드를 32비트의 압축된 메시지 자체와 의도적으로 겹쳤음을 주목하기 바란다.
    • 이 덕분에 하나의 32비트 정수를 그대로 읽고 쓸 수도 있고, 개별 필드를 따로 읽고 쓸 수도 있다.
    • 게다가 특정 필드 값을 변경할 때마다 32비트 값을 다시 조합할 필요가 없다.
NoteMesage n = new NoteMessage();
Console.WriteLine(n.PackedMsg);  // 0

n.Channel = 10;
n.Note = 100;
n.Velocity = 50;
Console.WriteLine(n.PackedMsg);  // 3302410

n.PackedMsg = 3328010
Console.WriteLine(n.Note);  // 200

공유 메모리

  • 메모리 대응 파일(memory-mapped file)이라고도 하는 공유 메모리(shared memory)는 한 컴퓨터의 여러 프로세스가 Remoting이나 WCF의 추가부담 없이도 자료를 공유할 수 있게 하는 Windows의 기능이다.
    • 공유 메모리는 극히 빠르며, 파이프와는 달리 공유 자료에 대한 임의 접근(random access)을 지원한다.
    • 15장에서 MemoryMappedFile 클래스를 이용해서 공유 자료에 접근하는 방법을 설명했는데, P/Invoke의 활용법을 잘 보여주는 예로 이번에는 그 클래스에 의존하지 않고 Win32 메서드들을 직접 호출해서 공유 메모리를 사용해 보기로 한다.
  • 공유 메모리를 할당하는 Win32 함수는 CreateFileMapping이다. 이 함수는 원하는 공유 메모리 영역의 크기(바이트 수)와 그 영열을 식별하는데 쓰이는 문자열 이름을 받는다.
    • 일단 이 함수로 공유 메모리를 생성했다면, 다른 응용 프로그램은 같은 이름으로 OpenFileMapping을 호출해서 그 공유 메모리에 접근한다.
    • 두 함수 모두 핸들을 돌려주는데, MapViewOfFile 함수를 이용해서 그 핸들을 포인터로 변환할 수 있다.
    • 다음은 공유 메모리 접근 기능을 캡슈로하한 클래스이다.
using System;
using System.Runtime.IneropServices;
using System.ComponentModel;

public sealed class SharedMem : IDisposable
{
  // 상수들을 열거형에 담아서 사용한다(이렇게 하면 형식 안전성이 좋아진다)
  enum FileProtection : uint // winnt.h의 상수들
  {
    ReadOnly = 2,
    ReadWrite = 4
  }

  enum FileRights : uint // WinBASE.h의 상수들
  {
    Read = 4,
    Write = 2,
    ReadWrite = Read + Write
  }

  static readonly IntPtr NoFileHandle = new IntPtr(-1);

  [DllImport("kernel32.dll", SetLastError = true)]
  static extern IntPtr CreateFileMapping(IntPtr hFile, int lpAttributes, FileProtection flProtect, uint dwMaximumSizeHigh, uint dwMaximumSizeLow, string lpName);

  [DllImport("kernel32.dll", SetLastError = true)]
  static extern IntPtr OpenFileMapping(FileRights dwDesirecAccess, bool bInheritHandle, string lpName);

  [DllImport("kernel32.dll", SetLastError = true)]
  static extern IntPtr MapViewOfFile (IntPtr hFileMappingObject, FileRights dwDesiredAccess, uint dwFileOffsetHigh, uint dwFileOffesetLow, uint dwNumberOfBytesToMap);

  [DllImport("kernel32.dll", SetLastError = true)]
  static extern bool UnmapViewOfFile(IntPtr map);

  [DllImport("kernel32.dll", SetLastError = true)]
  static extern int CloseHandle(IntPtr hObject);

  IntPtr fileHandle, fileMap;

  public IntPtr Root { get { return fileMap; } }

  public SharedMem(string name, bool existing, uint sizeInBytes)
  {
    if (exisiting)
      fileHandle = OpenFileMapping(FileRights.ReadWrite, false, name);
    else
      fileHandle = CreateFileMapping(NoFileHandle, 0, FileProtection.ReadWrit, 0, sizeInBytes, name);

    if (fileHandle == IntPtr.Zero)
      throw new Win32Exception();

    // 파일 전체에 대한 읽기/쓰기 맵을 얻는다.
    fileMap = MapViewOfFile(fileHandle, FileRights.ReadWrite, 0, 0, 0);

    if (fileMap == IntPtr.Zero)
      throw new Win32Exception();
  }

  public void Dispose()
  {
    if (fileMap != IntPtr.Zero) UnmapViewOfFile(fileMap);
    if (fileHandle != IntPtr.Zero) CloseHandle(fileHandle);
    fileMap = fileHandle = IntPtr.Zero;
  }
}
  • SetLastError 프로토콜을 통해서 오류를 보고하는 비관리 함수들에 대한 [DllImport] 특성들에 SetLastError=true가 지정되어 있음을 주목하기 바란다. 이렇게 하면 예외 발생 시 오류의 세부사항이 Win32Exception 예외에 채워진다.
    • 또한 Marshal.GetLastWin32Error를 호출해서 오류 부호를 명시적으로 조회할 수 있게 된다.
  • 이 클래스를 시험해 보려면 응용 프로그램이 두 개 필요하다. 첫 응용 프로그램은 다음과 같은 코드를 통해서 공유 메모리를 생성한다.
using (SharedMem sm = new SharedMem("MyShare", false, 1000))
{
  IntPtr root = sm.Root;
  // 이 프로세스가 공유 메모리의 소유자이다.
  Console.ReadLine();  // 이 지점에서 둘째 응용 프로그램을 실행해야 한다.
}
  • 둘째 응용 프로그램 역시 같은 이름으로 SharedMem 객체를 생성한다. 단, 공유 메모리를 새로 만드는 것이 아니라 기존 공유 메모리에 접근하려는 것이므로 existing 매개변수에 true를 지정한다.
using (SharedMem sm = new SharedMem("MyShare", true, 1000))
{
  IntPtr root = sm.Root;
  // 이제 기존 공유 메모리에 접근할 수 있다.
  // ...
}
  • 두 프로그램의 IntPtr 들은 같은 비관리 메모리 안을 가리키는 포인터이다. 그런데 이 공통의 포인터를 통해서 두 프로그램이 그 메모리를 좀 더 편하게 읽고 쓰려면 어떻게 해야 할까?
    • 한 가지 접근방식은 모든 공유 자료를 캡슐화하는 직렬화 가능 클래스를 만들고, UnmanagedMemoryStream을 이용해서 자료(그 클래스의 인스턴스)를 비관리 메모리에 직렬화(그리고 역직렬화)하는 것이다.
    • 그러나 자료가 많을 때는 이 방법이 비효율적이다. 예컨대 공유 메모리 클래스에 1MB 분량의 자료가 있는데, 그중 정수 하나만 갱신한다고 생각해 보면 이 방법이 얼마나 비효율적인지 이해가 될 것이다.
    • 더 나은 접근방식은 공유 자료를 하나의 구조체로 정의하고, 그것을 공유 메모리에 직접 대응시키는 것이다.

구조체를 비관리 메모리에 대응

  • [StructLayout] 특성이 Sequential이나 Explicit인 구조체는 비관리 메모리에 직접 대응(사상)된다. 다음과 같은 구조체를 생각해 보자.
[StuctLayout(LayoutKind.Sequential)]
unsafe struct MySharedData
{
  public int Value;
  public char Letter;
  public fixed float Numbers[50];
}
  • 여기서 fixed 지시자는 해당 배열 필드(값 형식 원소들을 담는 고정 길이 배열)를 구조체 자체에 포함시키는 (인라인화) 역할을 한다. 이 지시자 때문에 이 구조체는 비안전 코드(unsafe) 의 영역에 들어가게 된다.
    • 이 구조체를 인스턴스화 하면 부동소수점 수 50개를 담을 공간이 그 인스턴스 안에 실제로 할당된다.
    • 다시 말하면 표준 C# 배열과는 달리 Numbers 필드는 배열에 대한 참조가 아니라 배열 자체이다. 실제로 다음 코드를 실행하면
static unsafe void Main() => Console.WriteLine(sizeof(MySharedData));
  • 208이 출려된다. 4바이트 float 50개 더하기 4바이트 정수 하나(Value 필드) 더하기 2바이트 문자 하나(Letter 필드)면 206바이트이고, 거기에 4바이트 경계 정합(4바이트는 float 하나의 크기)을 위한 2바이트를 추가하면 208이 된다.
  • unsafe 문맥에서 MySharedData를 사용하는 방법을 보여주는 간단한 예로 다음 예제는 스택에 할당한 배열에 MySharedData를 적용한다.
MySharedData d;
MySharedData* data = &d; // d의 주소를 얻는다.

data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
  • 또는 다음과 같이 사용할 수도 있다.
// 배열을 스택에 할당한다.
MySharedData* data = stackalloc MySharedData[1];

data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
  • 물론 이 예제로 하는 일들은 모두 관리되는 문맥에서도 할 수 있다. 그렇지 않은 예로 다음 예제는 MySharedData 인스턴스를 CLR의 쓰레기 수거기가 미치지 못하는 비관리 힙에 저장한다. 비관리 포인터는 이런 용도로 사용할 때 정말로 유용하다.
MySharedData* data = (MySharedData*)Marshal.AllocHGlobal(sizeof(MySharedData)).ToPointer();

data->Value = 123;
data->Letter = 'X';
data->Numbers[10] = 1.45f;
  • Marshal.AllocHGlobal은 비관리 힙에 메모리를 할당한다. 이 메서드로 할당한 메모리를 나중에 해제하려면 다음과 같이 해야 한다.
Marshal.FreeHGlobal(new IntPtr(data));
  • 이처럼 메모리를 해제해 주지 않으면 유서 깊은 방식의 메모리 누수가 발생한다.
  • MySharedData라는 이름에 걸맞게 이번에는 이 구조체를 앞에서 작성한 SharedMem 클래스와 함께 사용해 보자. 다음 프로그램은 공유 메모리에 메모리 블록 하나를 할당하고 MySharedData 구조체를 그 메모리에 대응시킨다.
static unsafe void Main()
{
  using (SharedMem sm = new SharedMem("MyShare", false, 1000))
  {
    void* root = sm.Root.ToPointer();
    MySharedData* data = (MySharedData*) root;

    data->Value = 123;
    data->Letter = 'X';
    data->Numbers[10] = 1.45f;
    Console.WriteLine("공유 메모리에 기록했음");

    Console.ReadLine();

    Console.WriteLine("값은 " + data->Value);
    Console.WriteLine("문자는 " + data->Letter);
    Console.WriteLine("11번째 수는 " + data->Numbers[10]);
    Console.ReadLine();
  }
}
  • SharedMem 대신 .NET Framework의 MemoryMappedFile 클래스를 다음과 같이 사용해도 된다.
using (MemoryMappedFile mmFile = MemoryMappedFile.CreateNew("MyShare", 1000))
using (MemoryMappedViewAccessor accessor = mmFile.CreateViewAccessor())
{
  byte* pointer = null;
  accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
  void* root = pointer;
  ...
}
  • 다음은 이 프로그램과 메모리 블록을 공유하는 둘째 프로글매이다. 둘째 프로그램은 해당 블록에 구조체를 대응시켜서 첫 프로그램이 기록한 값을 읽는다(둘째 프로그램은 첫 프로그램이 ReadLine 호출에 걸려 있는 상태에서 실행해야 한다. 해당 using 문을 벗어나면 공유 메모리 객체가 처분되기 때문이다)
static unsafe void Main()
{
  using (SharedMem sm = new SharedMem("MyShare", false, 1000))
  {
    void* root = sm.Root.ToPointer();
    MySharedData* data = (MySharedData*) root;

    Console.WriteLine("값은 " + data->Value);
    Console.WriteLine("문자는 " + data->Letter);
    Console.WriteLine("11번째 수는 " + data->Numbers[10]);

    // 더 나아가서, 공유 메모리를 갱신해 본다.
    data->Value++;
    data->Letter = '!';
    data->Numbers[10] = 987.5f;
    Console.WriteLine("공유 메모리를 갱신했음");
    Console.ReadLine();
  }
}
  • 두 프로그램의 출력은 다음과 같다.
첫 프로그램
공유 메모리에 기록했음
값은 124
문자는 !
11번째 수는 987.5

둘째 프로그램
값은 123
문자는 X
11번째 수는 1.45
공유 메모리를 갱신했음
  • 포인터를 너무 두려워할 필요는 없다. C++ 프로그래머들은 응용 프로그램 전체에서 포인터를 사용하지만, 그래도 응용 프로그램이 잘 돌아간다.
  • 사실 이 예제들은 실제로 안전하지 않은데, unsafe 키워드나 포인터를 사용했기 때문이 아니라 두 프로그램이 같음 메모리에 동시에 접근할 때 발생하는 스레드 안전성(좀 더 정확히 말하면 프로세스 안전성)을 고려하지 않았기 때문이다.
    • 실제 응용 프로그램에서는 MySharedData 구조체의 Value 필드와 Letter 필드에 volatile 키워드를 추가해서 CPU 레지스터에 캐싱되지 않게 해야 한다.
    • 더 나아가서 필드들과의 상호작용이 지금 예제보다 더 복잡해진다면, 여러 프로세스가 공유하는 Mutex를 이용해서 접근을 보호해야 할 가능성이 크다.

fixed와 fixed{…}

  • 어떤 구조체를 메모리에 직접 대응시킬 때 적용되는 제약 하나는 그 구조체의 피르들이 모두 비관리 형식이어야 한다는 것이다.
    • 예컨대 문자열 자료를 공유하려면 C#의 문자열 대신 고정 길이 문자 배열을 사용해야 하며, 따라서 공유 메모리에 문자열을 읽고 쓸 때마다 string 형식과의 변환이 필요하다.
    • 다음은 그러한 변환 방법을 보여주는 예이다.
[StructLayout(LayoutKind.Sequential)]
unsafe struct MySharedData
{
  ...
  // 문자 200개(즉 400바이트)를 담을 공간을 할당한다.
  const int MessageSize = 200;
  fixed char message [MessageSize];

  // 이 코드는 보조(helper) 클래스에 따로 두는 것이 바람직할 것이다.
  public string Message
  {
    get { fixed (char* cp = message) return new string(cp); }
    set 
    {
      fixed(char* cp = message)
      {
        int i = 0;
        for (; i < value.Length && i < MessageSize - 1; i++)
          cp[i] = value[i];

        // 널 종료 문자를 추가한다.
        cp[i] = '\0';
      }
    }
  }
}
  • 고정 배열에 대한 참조라는 것은 없다. 고정 배열은 항상 포인터를 통해서 접근된다. 색인으로 고정 배열의 원소에 접근하면 실제로 포인터 산술이 일어난다.
  • fixed 키워드가 적용된 첫 문장은 구조체 안에 문자 200개를 담을 공간을 직접 할당한다.
    • 그러나 속성 접근 메서드 안에 적용된 fixed 키워드는 첫 문장의 fixed 와는 다른 의미로 스인다(이는 fixed의 다소 헷갈리는 측면이다.)
    • 두 번쨰 fixed는 CLR에게 이 객체를 고정(pinning)하라고 지시하는 역할을 한다. 이렇게 하면 CLR이 fixed 블록 안에서 해당 객체를 수거한다고 결정해도, 메모리 힙에서 바탕 구조체를 이동하지 않는다.
    • fixed 블록 안에서 아직 그 내용을 직접적인 메모리 포인터를 통해서 반복하는 중이므로, 이처럼 이동을 금지해야 문제가 발생하지 않는다.
    • 코드를 살펴보면 MySharedData 가 메모리 안에서 이동할 이유가 없어 보인다. MySharedData는 관리되는 힙이 아니라 쓰레기 수거기의 영향권에서 벗어난 비관리 세계에 있기 때문이다.
    • 그러나 컴파일러는 이 점을 모르며, 어쩌면 프로그램이 MySharedData를 관리하는 문맥에서 사용할 수도 있다고 생각한다.
    • 이 때문에 관리되는 문맥에서 unsafe를 안전하게 실행하려면 이처럼 fixed 키워드를 명시적으로 적용할 필요가 있다.
    • 그리고 컴파일러가 아예 틀린 것도 아니다. 다음과 같은 코드는 MySharedData를 힙에 할당한다.
object obj = new MySharedData();
  • 이렇게 하면 MySharedData는 박싱된 객체로서 관리되는 힙에 존재하며, 따라서 쓰레기 수거 과정에서 이동할 수 있다.
  • 이상의 예제는 문자열을 비관리 메모리에 대응된 구조체로 표현하는 방법을 보여준다.
    • 좀 더 복잡한 형식이라면 그냥 기존의 직렬화 코드를 사용할 수도 있다.
    • 이때 주의할 점은 직렬화된 자료의 길이가 구조체에 할당된 공간을 넘어서는 안 된다는 것이다. 만일 직렬화된 자료가 너무 길면 이후의 필드들과 겹칠 것이며, 그러면 구조체가 뜻하지 않게 공용체처럼 작동하게 될 것이다.

COM 상호 운용성

  • .NET 런타임은 첫 버전부터 COM을 특별하게 지원했다. 덕분에 COM 객체를 .NET에서(그리고 그 반대로) 사용할 수 있었다. C# 4.0에서는 이러한 지원이 크게 향상되어서, 사용성은 물론 배치 방식도 개선되었다.

COM의 목적

  • Component Objecte Model(구성요소 객체 모형)의 약자인 COM은 여러 API를 위한 이진 코드 표준으로 Microsoft가 1993년에 발표했다.
    • Microsoft가 COM을 제정한 동기는 여러 구성요소가 언어 독립적인, 그리고 버전내구적인 방식으로 통신하게 하는 것이었다.
    • COM이 나오기 전 Windows가 사용하던 전략은 C 프로그래밍 언어로 선언된 구조체들과 함수들을 DLL에 담아서 배포하는 것이었다.
    • 이러한 접근 방식은 언어에 종속적일 뿐만 아니라 견고하지도 않았다.
    • 그러한 DLL에 담긴 한 형식의 명세는 그 구현과 분리되지 않았기 때문에, 구조체에 필드 하나를 추가하기만 해도 명세가 깨졌다.
  • COM의 장점은 형식의 명세를 COM 인터페이스라고 알려진 구축 요소를 통해서 그 바탕 구현과 분리한다는 것이다. 또한 COM은 단순한 프로시저 호출을 넘어서 상태 있는(stateful) 객체에 대한 메서드 호출도 가능하게 만들었다.
  • 어찌보면 .NET의 프로그래밍 모형은 COM 프로그래밍 원리들이 진화한 형태라 할 수 있다. COM처럼 .NET도 다중 언어 개발을 지원하며, 이진 구성요소가 변해도 그것에 의존하는 응용 프로그램이 깨지지 않는다.

COM 형식 체계의 기초

  • COM 형식 체계(type system)은 인터페이스를 중심으로 구성된다. COM 인터페이스는 .NET 인터페이스와 비슷하지만, COM 형식의 기능성이 오직 인터페이스를 통해서만 노출된다는 점에서 그 중요성이 더 크다.
    • 반면 .NET 세계에서는 다음 예처럼 인터페이스 없이 형식만 간단하게 선언할 수 있다.
public class Foo
{
  public string Test() => "Hello world";
}
  • 이 형식을 사용하는 코드는 Foo를 직접 인스턴스화해서 사용할 수 있다. 그리고 나중에 Test 메서드의 구현이 바뀌어도 그 메서드를 호출하는 클래스를 다시 컴파일할 필요가 없다.
    • 이러한 점에서 .NET은 인터페이스와 구현을 ‘인터페이스 없이’ 분리한다고 할 수 있다.
    • 심지어 다음과 같은 중복적재 버전을 추가해도 호출자에 영향을 미치지 않는다.
public string Test(string s) => "Hello world " + s;
  • 그러나 COM 세계에서는 Foo의 기능성을 인터페이스를 통해 노출함으로써 이러한 분리성을 달성한다. 즉, Foo의 형식 라이브러리에 이를테면 다음과 같은 인터페이스가 존재해야 한다.
public interface IFoo { string Test(); }
  • 이것은 독자의 이해를 돕기 위해 예로 든 C# 인터페이스일 뿐, 실제 COM 인터페이스가 아니다. 어쨌든 원리는 동일하다. 단지 표현 수단이 다를 뿐이다.
  • 그리고 호출자는 Foo가 아니라 IFoo와 상호작용한다.
  • Test의 중복적재 버전 추가에 관련한 상황은 COM이 .NET 보다 복잡하다.
    • 우선 IFoo 인터페이스는 수정하지 말아야 한다. 인터페이스가 수정되면 이전 버전과의 이진 호환성이 깨지기 때문이다(COM의 원리 중 하나는, 일단 발표된 인터페이스는 불변이(immutable)라는 것이다)
    • 둘째로 애초에 COM은 중복적재를 지원하지 않는다. 중복적재를 흉내내는 방법은 Foo가 다음과 같은 또 다른 인터페이스를 구현하게 하는 것이다.
public interface IFoo2 { string Test(string s); }
  • 이처럼 다중 인터페이스 지원은 COM 라이브러리의 버전 변화에 핵심적인 기능이다.

IUnknown 인터페이스와 IDispatch 인터페이스

  • 모든 COM 인터페이스에는 식별용 GUID가 있다.
  • COM 형식 체계의 뿌리에 해당하는 인터페이스는 IUnknown이다. 모든 COM 객체는 반드시 이 인터페이스를 구현해야 한다. 이 인터페이스에는 다음 세 메서드가 있다.
    • AddRef
    • Release
    • QueryInterface
  • AddRef와 Release는 객체의 수명 관리를 위한 메서드들이다.
    • COM은 자동 쓰레기 수거가 아니라 참조 계수(reference counting) 방식으로 객체들의 수명을 관리하기 때문에 이런 메서드들이 필요하다(애초에 COM은 쓰레기 수거가 적합하지 않은 비관리 코드에 쓰이도록 설계된 것이다)
    • QueryInterface 메서드는 특정 인터페이스를 지원하는 객체 참조를 돌려준다(그런 참조가 있다면)
  • 동적 프로그래밍(이를테면 스크립팅과 Automation)이 가능하려면 COM 객체가 IDispatch도 구현해야 한다. 이를 통해서 이를테면 VBScript에서 COM 객체를 지연 바인딩 방식으로 호출할 수 있다.
    • 이는 C#의 dynamic과 비슷한 방식이다.

C#에서 COM 구성요소 호출

  • CLR은 COM 지원 기능을 내장하고 있으므로, C# 코드에서 IUnknown과 IDispatch를 직접 다룰 필요는 없다. 그냥 .NET 객체를 그대로 사용하면 된다.
    • 그러면 RCW(runtime-callable wrapper; 런타임 호출 가능 래퍼)라고 부르는 일종의 프록시를 통해서 런타임이 해당 호출을 COM 세계로 인도한다(마샬링)
    • 런타임은 또한 AddRef와 Release를 적절히 호출해서(.NET 객체가 생성 및 종료(finalization) 될 때) 객체의 수명을 관리해 주며, 두 세계 사이의 기본 형식 변환도 처리해 준다.
    • 그러한 형식 변환 덕분에 .NET 세계의 코드와 COM 세계의 코드는 이를테면 정수나 문자열 형식을 자신에게 익숙한 형태 그대로 사용하 ㄹ수 있다.
  • 또한 동적이 아니라 정적 형식에 맞는 방식으로 RCW에 접근하는 방법도 필요하다.   이를 담당하는 것이 COM 상호운용 형식(COM Interop type)들이다.
    • COM 상호운용 형식은 주어진 COM 형식의 각 멤버에 대응되는 .NET 멤버들을 노출하는 프록시 형식인데, 프로그래머가 직접 정의할 필요는 없다.
    • 명령줄에서 실행하는 형식 라이브러리 임포터 도구(tlbimp.exe)는 지정된 COM 라이브러리에 기초해서 COM 상호운용 형식들을 생성하고, 그것들을 하나의 COM 상호운용 어셈블리로 컴파일한다.
    • COM 구성요소가 여러 개의 인터페이스를 구현하는 경우 tlbimp.exe 도구는 모든 인터페이스 멤버들의 합집합을 담은 하나의 형식을 생성한다.
  • Visual Studio에서도 COM 상호운용 어셈블리를 만들 수 있다.  ‘참조 추가’ 대화상자의 COM 탭에서 원하는 COM 라이브러리를 선택하면 된다.
    • 예컨대 독자의 컴퓨테어 Microsoft Excel 2007이 설치되어 있다고 할 때, COM 탭에서 Microsoft Excel 12.0 Office Library를 선택하면 Excel의 COM 클래스들을 C#에서 사용할 수 있게 된다.
    • (예시 코드 생략)

선택적 매개변수와 명명된 인수

  • COM API는 함수 중복적재를 지원하지 않기 때문에, COM 메서드들을 보면 매개변수가 많고 그중 다수가 선택적(생략 가능)인 경우가 흔하다.
    • (예시 코드 생략)
  • 또한 명명된 인수(named argument) 기능 덕분에 추가적인 인수를 위치와 무관하게 지정할 수 있다.

암묵적 ref 매개변수

  • 일부 COM API의 함수들은 매개변수 값을 수정하지 않더라도 모든 매개변수를 참조 전달로 선언한다. 이는 인수 값을 복사하면 성능이 나빠진다는 오랜 믿음 때문이다(그러나 모든 매개변수를 참조 전달로 바꾸어도 실질적인 성능 이득은 무시할 만한 수준이다)
  • 예전에는 그런 메서드를 C#에서 호출하려면 코드가 상당히 지저분했다. 그런 함수를 호출하려면 모든 인수에 ref 키워드를 붙여야 하며, 그러면 선택적 매개변수를 사용할 수 없기 때문이다.
    • (예시 코드 생략)
  • 그러나 C# 4.0부터는 COM 함수 호출 시 ref 수정자를 생략할 수 있으며 따라서 다음처럼 선택적 매개변수 기능을 사용할 수 있다.
word.Open("foo.doc");

인덱서

  • ref 수정자 생략 능력에는 또 다른 장점이 있다. 바로 ref 매개변수가 있는 COM 인덱서에 보통의 C# 인덱서 구문으로 접근할 수 있다는 것이다.
    • C# 인덱서는 ref/out 매개변수를 지원하지 않으므로, ref 수정자 생략 능력이 없다면 COM에서도 그러한 접근이 불가능했을 것이다.(ref 수정자를 생략할 수 없었던 C# 이전 버전들에서는 get_XXX와 set_XXX 같은 배경 메서드를 호출하는 다소 번거로운 우회책을 사용했는데, 하위 호환성을 위해서는 이 우회책이 여전히 유효하다)
  • C# 4.0에서는 인덱서와의 상호운용 능력이 더욱 개선되었다. 예컨대 이제는 인수를 받는 COM 속성을 인덱서 구문을 통해서 호출할 수 있다.
    • 다음 예에서 Foo는 정수 인수를 받는 속성이다.
myComObject.Foo[123] = "Hello";
  • 아직도 C#에서는 이런 속성을 독자가 직접 정의할 수 없다. 하나의 형식은 오직 그 형식 자체에 대한 인덱서(‘기본’ 인덱서)만 노출할 수 있다.
    • 따라서 C# 코드에서 위와 같은 문장이 유효하려면 Foo는 인덱서(기본 인덱서)를 노출하는 또 다른 형식을 돌려주어야 한다.

동적 바인딩

  • 동적 바인딩이 COM 구성요소의 호출에 도움이 되는 이유는 두 가지이다. 첫째로 COM 상호운용 형식 없이도 COM 구성요소에 접근하는 것이 가능한데, 이러한 능력은 바로 동적 바인딩 덕분이다.
    • COM 구성요소 형식의 이름으로 Type.GetTypeFromProgID를 호출하면 그 형식을 대표하는 Type 객체를 얻을 수 있으며, 그 객체를 이용해서 COM 인스턴스를 생성해서 원하는 멤버를 호출할 수 있다.
    • 이는 모두 동적 바인딩 덕분이다. 물론 이런 코드에 대해서는 IntelliSense 지원이나 컴파일 시점 점검이 불가능하다.
Type exelAppType = Type.GetTypeFromProgID("Excel.Application", true);
dynamic excel = Activator.CreateInstace(excelAppType);
excel.Visible = true;
dynamic wb = excel.Workbooks.Add();
excel.Cell[1, 1].Value2 = "foo";
  • 동적 바인딩 대신 반영 기능을 이용해서도 같은 결과를 얻을 수 있지만 코드가 훨씬 지저분해진다.
  • 이 주제의 한 변형은 IDispatch만 지원하는 COM 구성요소를 호출하는 것이다. 그러나 그런 구성요소는 상당히 드물다.
  • 동적 바인딩이 COM 호출에 도움이 되는 또 다른 이유는(첫 번째 이유보다는 도움이 덜하지만) 동적 바인딩을 통해서 COM variant 형식을 좀 더 수월하게 다룰 수 있다는 것이다.
    • 필요성보다는 잘못된 설계의 탓이 더 크긴 하지만, COM API 함수들에는 이 variant 형식이 자주 쓰인다.
    • 아주 대충 말하자면 이 형식은 .NET의 object 형식에 해당한다.
    • Visual Studio 솔루션 탐색기에서 특정 참조를 선택한 후 참조 속성 중 ‘Interop 형식 포함’을 활성화하면 런타임은 variant를 object에 대응시키는 것이 아니라 dynamic에 대응시킨다.
    • 이렇게 하면 C# 코드에서 캐스팅을 생략할 수 있다. 예컨대
var range = (Excel.Range)excel.Cells[1, 1];
range.Font.FontStyle = "Bold";
  • 라고 하는 대신
excel.Cells[1, 1].Font.FontStyle = "Bold";
  • 라고 해도 된다.
  • 이런 방식의 단점은 코딩시 Visual Studio의 자동 완성 기능의 지원을 받지 못한다는 것이다. 따라서 Font 라는 속성이 존재함을 미리 알고 있어야 한다.
    • 이 때문에 보통은 그냥 결과를 알려진 상호운용 형식에 동적으로 배정하는 것이 더 쉽다.
    • 다음이 그러한 예이다.
Excel.Range range = excel.Cells[1, 1];
range.Font.FontStyle = "Bold";
  • Visual Studio 2010부터는 COM 상호운용 어셈블리 참조에 대해서는 ‘Interop 형식 포함’이 기본적으로 활성화되며, 이에 의해 variant가 dynamic에 대응된다.

상호운용 형식들을 응용 프로그램에 내장

  • 이전에 보았듯이 보통의 경우 C#은 tlbimp.exe 도구로 생성된 상호운용 형식들을 통해서 COM 구성요소를 호출한다
  • 예전에는 상호운용 형식들을 담은 어셈블리들을(다른 종류의 어셈블리들도 마찬가지지만) 참조만 할 수 있었다.
    • 그런데 복잡한 COM 구성요소들을 담은 상호운용 어셈블리는 덩치가 상당히 클 수 있다는 점이 문제였다.
    • 예컨대 Microsoft Word를 위한 작은 애드인을 실행하려면 그 애드인 자체보다 수십 배 큰 상호운용 어셈블리가 필요했다.
  • C# 4.0부터는 상호운용 어셈블리를 참조하는 대신 링크할 수 있게 되었다.
    • 어셈블리를 링크하면 컴파일러는 그 어셈블리에서 응용 프로그램이 실제로 사용하는 형식들과 멤버들을 파악해서, 그것들의 정의를 응용 프로그램 자체에 직접 내장(포함)한다.
    • 이 방식에서는 실제로 쓰이는 COM 인터페이스만 응용 프로그램에 포함되므로, 응용 프로그램 패키지의 크기를 크게 줄일 수 있다.
  • Visual Studio 2010과 그 이후 버전들에서는 COM 상호운용 어셈블리의 링크가 기본적으로 활성화된다.
    • 이를 비활성화하려면 솔루션 탐색기에서 해당 참조를 선택한 후 참조 속성 시트의 ‘Interop 형식 포함’을 False로 설정하면 도니다.
  • 명령줄 컴파일러에서 상호운용 어셈블리 링크를 활성화하려면 csc 호출 시 /reference 대신 /link를 (또는 /R 대신 /L을) 지정하면 된다.

형식 동치

  • CLR 4.0과 그 이후 버전들은 링크된 상호운용 형식들에 대한 형식 동치(type equivalence)를 지원한다.
    • 무슨 말이냐면 두 어셈블리가 각자 어떤 상호운용 형식을 링크했을 때, 만일 그 형식들이 동일한 COM 형식을 감싼다면 두 상호운용 형식이 동등한 형식으로 간주된다는 뜻이다.
    • 심지어 그 어셈블리들이 링크하는 상호운용 어셈블리들이 각자 개별적으로 생성된 경우에도 그렇다.
  • 형식 동치는 System.Runtime.InteropServices 이름공간의 TypeIdentifierAttribute 특성에 의존한다.
    • 상호운용 어셈블리를 링크하면 컴파일러가 자동으로 이 특성을 적용한다. 그러면 GUID가 같은 COM 형식들은 모두 동치로 간주된다.
  • 형식 동치 지원 덕분에 기본 상호운용 어셈블리의 필요성이 사라졌다.

기본 상호운용 어셈블리

  • C# 4.0 이전에는 상호운용 링크도, 형식 동치도 없었다. 그래서 두 개발자가 같은 COM 구성요소에 대해 각자 tlbimp.exe를 실행해서 만든 상호운용 어셈블리들이 호환되지 않았으며, 결과적으로 상호운용성이 훼손되었다.
    • 이에 대한 우회책은 COM 라이브러리 작성자가 공식적으로 상호운용 어셈블리를 만들어서 배포하는 것이었는데, 그런 공식 버전의 어셈블리를 기본 상호운용 어셈블리(primary interop assembly, PIA)라고 부른다.
    • 4.0 이전의 C#에 기초한 코드가 아직 많이 남아 있으므로 PIA도 여전히 중요하다.
  • 사실 PIA는 좋은 해결책이 아니다.
    • (이하 내용 생략)

COM 코드에서 C# 객체 사용

  • C# 클래스를 COM에서 사용할 수 있게 하는 것도 가능하다. CLR은 CCW(COm-callabel wrapper; COM 호출 가능 래퍼)라고 부르는 프록시를 통해서 이러한 기능을 지원한다.
    • CCW는 두 세계 사이에서 형식들을 인도한다(RCW가 하는 것처럼)
    • CCW는 COM 프로토콜이 요구하는 IUnknown을(경우에 따라서는 IDispatch도) 구현한다.
    • CCW 객체의 수명은 COM 쪽에서 참조 계수 방식으로(CLR의 쓰레기 수거가 아니라) 관리한다.
  • 공용 클래스이면 그 어떤 것도 COM 쪽에 노출할 수 있다. 단 [Guid] 특성을 이용해서 어셈블리 자체에 GUID를 부여해야 한다.
    • 그 GUID는 어셈블리로부터 생성할 COM 형식 라이브러리를 고유하게 식별하는 용도로 쓰인다.
[assembly: Guid("...")]  // COM 형식 라이브러리를 위한 고유한 GUID
  • 기본적으로 그 라이브러리의 모든 공용 형식을 COM 쪽에서 볼 수 있다. 특정 형식만 노출하려면 [ComVisible] 특성을 사용해야 한다.
    • 예컨대 몇몇 형식만 노출하고 싶다면 어셈블리 자체에 [ComVisible(false)]를 지정해서 모든 형식을 감춘다음, 노출할 형식에만 따로 [ComVisible(true)]를 지정하면 된다.
  • 마지막으로 할 일은 tlbexp.exe 도구를 실행하는 것이다.
tlbexp.exe myLibrary.dll
  • 이러면 하나의 COM 형식 라이브러리 파일(.tlb)이 만들어진다. 그것을 시스템에 등록한 후 COM 응용 프로그램에서 사용하면 된다. COM 노출 클래스들과 부합하는 COM 인터페이스들은 자동으로 생성된다.
[ssba]

The author

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

댓글 남기기

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