우연히 printf 계열의 함수들에 대한 플랫폼 호출 방법에 대한 자료를 찾는 도중 재미있는 글을 발견하였습니다. C#의 공식 사양에 포함되지 않은 Microsoft C# 컴파일러의 특수한 기능들 (Mono나 DotGNU의 C# 컴파일러에는 존재하지 않거나 의도하지 않은 예외가 발생할 수도 있습니다.)을 알게 되었습니다.

주의: 비공식 키워드를 사용하는 것에 대한 책임은 프로그래머에게 달려있습니다. 다른 언어들과의 호환성 문제와 부딪히지 않으려면 이러한 비공식 키워드의 사용에 주의해야만 합니다.

TypedReference에 대한 비공식 키워드

TypedReference 형식은 관리되는 객체에 대한 명확한 주소 정보 및 핸들 정보를 가리키는 특수한 객체입니다. TypedReference 형식의 객체를 만들기 위한 비공식 키워드가 C#에 존재합니다.

__makeref(변수) : 괄호 안에 TypedReference 형식으로 조사하기를 원하는 객체를 지정하면 되는데, 여기에는 값 형식의 변수가 올 수도 있습니다. 예를 들면 다음과 같습니다.

int x = 0;
TypedReference xt = __makeref(x);

__reftype(TypedReference 객체) : 괄호 안에 TypedReference 객체를 지정하면 TypedReference 객체가 원래 가리키고 있던 객체의 형식 정보를 반환합니다. 예를 들어, 위의 예에서 System.Int32에 대한 TypedReference를 만들었는데 이 키워드를 사용함으로서 System.Int32에 대한 Type 객체가 반환됩니다.

int x = 0;
TypedReference xt = __makeref(x);
Type xt2 = __reftype(xt);

__refvalue(TypedReference 객체, 가져오기 원하는 형식) : 괄호 안에 TypedReference 객체를 첫 인수로 지정하고, TypedReference가 가리키고 있는 실제 객체에 대한 형식명을 두 번째 인수로 지정합니다. 이 키워드를 이용하여 TypedReference 객체로부터 곧바로 원래의 객체를 가져올 수 있습니다.

int x = 0;
TypedReference xt = __makeref(x);
Type xt2 = __reftype(xt);
int y = __refvalue(xt, int);

printf, sprintf, fprintf와 같이 C 언어에서만 사용하는 가변 인수를 C#에서도 도입하기

va_args, va_start, va_end와 같은 매크로 함수를 혹시 기억하십니까? printf 같이 인수를 가변적으로 조절할 수 있는 함수를 디자인하기 위하여 사용했던 함수들입니다. 이러한 함수들을 흉내내기 위하여 C#에서는 params 키워드를 제공하였고 그 결과 가변 인수처럼 동작하면서도 결과적으로 1차원 배열을 다룰 수 있는 향상된 가변 인수 처리법을 제공할 수 있게 되었습니다. 하지만 이렇게 변경된 가변 인수 처리법은 종전의 C 언어 스타일과는 크게 다르기 때문에 C 언어를 위하여 디자인된 printf 계의 함수들은 사용할 수 없는 것 처럼 이야기되어왔습니다. 하지만 지금 소개하는 __arglist 키워드를 이용함으로서 이런 한계를 극복할 수 있습니다.

[SecurityPermission(SecurityAction.LinkDemand)]
[DllImport(ModuleName, CharSet = CharSet.Ansi)]
protected static extern int _cprintf(string fmt, __arglist);

__arglist라는 키워드를 주목합니다. 매개 변수 형식 선언이 아님에도 불구하고 사용할 수 있다는 것이 매우 특이한데, 바로 이것이 C 언어 스타일의 가변 인수여야 함을 컴파일러에게 알려주는 힌트가 됩니다. 하지만 이렇게 선언하면 뒷쪽에 가변 인수를 제공하기 위하여 __arglist() 키워드를 사용하여야 하지만, __arglist라는 키워드를 지원하지 못하는 다른 프로그래밍 언어에서는 사용할 수 없습니다.

이제 위의 _cprintf 함수를 부르는 예제를 보기로 합니다.

int result = _cprintf("%d", __arglist(30));

그렇습니다. __arglist라는 가상의 매크로에 필요한 만큼 인수를 던져주면 됩니다. 그러면, 여기서 궁금한 점이 하나 더 생기는데 __arglist를 params 키워드 대신해서 사용할 수도 있을까에 대한 의문입니다. 그 답은 "Yes"입니다. 다음의 코드를 보겠습니다.

protected void Page_Load(Object sender, EventArgs e)
{
   int x=85;
   string y = "a stringy thingy";
   double d=19.45;
   WriteToPage(__arglist(x,y,d));
}

public  void WriteToPage(__arglist)
{
   ArgIterator ai = new ArgIterator(__arglist);
   while(ai.GetRemainingCount() >0)
   {
      TypedReference tr = ai.GetNextArg();
      Response.Write(TypedReference.ToObject(tr)+"<BR>");
   }
}

int, string, double 형의 변수를 선언하고 __arglist(x,y,d)로 이들 인수를 던집니다. 그러면 WriteToPage 메서드에서는 이렇게 받아들인 인수들을 조회하기 위하여 ArgIterator 객체를 사용하여 하나씩 조회합니다. ArgIterator 객체로 조회한 매개 변수들은 TypedReference로 포장된 객체들이므로 실제 객체를 위의 코드처럼 TypedReference.ToObject 메서드로 가져오거나 __refvalue 키워드를 이용하여 가져와서 사용하는 것입니다.

자료 출처: http://www.eggheadcafe.com/articles/20030114.asp
Creative Commons License
Creative Commons License
남정현 이 작성.

당신의 의견을 작성해 주세요.

  1. Comment RSS : http://rkttu.com/rkttublog/rss/comment/194
  2. OpenID Logo공도 2007/11/20 01:20  편집/삭제  댓글 작성  댓글 주소

    댓글 남겨주신 것 보고 따라 들어왔더니 이런 좋은 정보가!
    마치 VB에서 ObjPtr에 대해 알게 된 것 같은 기분이 들어요 :)

[로그인][오픈아이디란?]
오픈아이디로만 댓글을 남길 수 있습니다
Visual C++ 8.0의 ATL 프로젝트를 이용하여 간단히 통신 컴포넌트를 제작해 보았다. 여러 가지를 배울 수 있었는데 무엇보다도 ATL 프로젝트에서 발생하기 쉬운 여러가지 미궁들에 대한 정확한 해답을 배웠다는 점이다. 그리고 ATL과 Visual C++ 컴파일러가 제공하는 "특성" 프로그래밍의 덕을 톡톡히 봤다.

하지만 Visual C++ 8.0이기 때문에 치루어야 했던 문제가 하나 있다. 바로 Visual C++ 8.0 Runtime Distribution을 반드시 고객 PC에 설치해야 한다는 엄청난 문제이다. 응용프로그램과 Visual C++ 런타임 버전을 서로 격리하기 위한 목적으로 Native Assembly Cache가 필요했고 그에 따라 별도의 Distribution Pack을 이용하여 캐쉬를 구성해야 했다는 것은 분명히 납득할만한 설명이지만 이건 .NET Framework 2.0을 설치하는 것과 전혀 다르지 않은 양상이다. 물론, Windows Installer로 포장을 해도 간단히 감추어지고 .NET Framework 2.0 전체를 다 설치하는것보다는 상황이 좋지만 원래 Visual C++ 프로그램이 가졌던 고유의 장점이 일시에 물거품이 되버린 셈이다.

내가 원하는 환경을 사용할 수 있으면서도 이런 문제를 피해가기 위해서는 아무래도 VIsual C++ 7.1 버전을 이용하는 것이 바람직할 것으로 보인다.
Creative Commons License
Creative Commons License
남정현 이 작성.

당신의 의견을 작성해 주세요.

[로그인][오픈아이디란?]
오픈아이디로만 댓글을 남길 수 있습니다

C와 C++에서 사용하는 버클리 소켓은 데이터를 주거나 받을 때 char 형식의 배열을 이용한다. .NET Framework에서도 char 형식과 가장 근접한 데이터 형식인 byte 형식의 배열을 이용한다. 하지만 약간의 호기심이 생겼다. 생각없이 사용하는 이들 소켓 API의 두 데이터 타입은 과연 숫자 범위까지 같은것일까?

C와 C++의 char 데이터 형식은 -128 ~ +127까지를 표현할 수 있다. 반면 .NET Framework의 byte는 0 ~ +255까지 표현이 가능하다. 정작 C와 C++과 호환성을 유지하기 위해서는 byte가 아닌 sbyte여야 했는데 왜 byte를 사용하게 된 것일까? sbyte는 char와 완전히 같은 데이터 범위를 보장함과 동시에 크기도 같다. 하지만 CLSCompliant(false) 플래그가 붙어있고 sbyte를 기반으로 구현된 소켓 API는 닷넷에 없다.

그래서 간단히 실험을 해보았다.

우선 .NET Framework의 소켓에서 BSD 소켓으로 데이터를 보내보았다. 물론 두 데이터 타입이 특별한 처리를 하지 않고도 표현할 수 있는 범위의 수인 0 ~ 127까지는 특이한 점이 없었다. 하지만 128 ~ 255의 값은 BSD 소켓에서 어떻게 받아들여질까? 대강 예상하고 있었고 실제로도 그렇게 되었는데, 음수로 뒤집어서 전달되었다. 순환 오버플로우와 유사하다는 생각이 들었다.

이번엔 반대로 BSD 소켓에서 .NET Framework의 소켓으로 데이터를 보내보았다. 위와 마찬가지로 0 ~ 127까지는 있는 그대로 전달되었다. 하지만 -128 ~ -1까지의 값은 음수로 전송되었지만 .NET Framework의 입장에서는 127 이후부터 255 사이의 양수값으로 바뀌어 들어갔다. 이 역시 순환 오버플로우와 유사한 동작이었다.

영양가 없는 실험이었지만 나름 궁금함은 풀어볼 수 있어서 좋았던것 같다. 그리고 결론을 하나 더 얻었는데, 실질적으로 우리가 네트워크를 통해서 전송하고자 하는 데이터의 범위는 0 ~ 127 안일 확률이 매우 높다. 2바이트 문자열과 유니코드 문자열도 결국은 이 범위 안에서 처리하도록 만들어지게 된다고 생각할 수 있겠다.

Creative Commons License
Creative Commons License
남정현 이 작성.

당신의 의견을 작성해 주세요.

[로그인][오픈아이디란?]
오픈아이디로만 댓글을 남길 수 있습니다