C++ CLR로 만드는 간단한 ASP.NET 웹 사이트

닷넷 프레임워크 기술들 중 가장 적게 알려지고 그 비중이 많이 축소되어 소개되는 부문이 바로 Visual C++ CLR일 것입니다. 생각보다 C++ CLR이 유용할 수 있음에도 불구하고 여론에 떠밀려서 거의 사장되다시피하고 있지요. 이번 블로그 포스트에서는 C++ CLR을 통해서 실용적인 코딩 하나를 해볼까합니다.

C++과 더불어 관련 라이브러리들 (STL, ATL, WTL, Boost, MFC 등)의 경우 일반적인 응용프로그램을 작성하거나, 성능 튜닝이 필요한 응용프로그램들을 작성하거나, 수학 라이브러리 등의 힘을 빌어 불필요한 오버헤드가 없는 고속 연산을 처리하는 등의 목적에 알맞게 디자인되어있습니다. 그러나 엔터프라이즈 프로그래밍의 경우에서처럼 구조화된 처리를 수행해야 하는 경우 상당히 불편한 점이 많습니다. 간단한 예로 당장 XML 문서 하나 분석하는 것도 썩 편리하지는 않지요.

Visual Studio 패밀리에서 공식적으로 제공되는 기능은 아니지만 C++ CLR도 닷넷 프레임워크 위에서 기동되는 어셈블리를 작성할 수 있습니다. 그리고 더 중요한 것은 기존의 x86 코드나 x64 코드를 MSIL 어셈블리 사이에 끼워넣을 수 있다는 점입니다. 이러한 사실을 바탕으로 하여 하이브리드 코드를 만들 수 있습니다.

그럼 하나씩 살펴보도록 하겠습니다.

  1. C++ CLR 기반의 클래스 라이브러리 프로젝트 하나를 만듭니다. 프로젝트 이름으로 여기서는 “CLRxASPX”를 사용하기로 하겠습니다.

  2. 여느 닷넷 프로젝트처럼 예상 가능한 종류의 파일들이 만들어집니다. AssemblyInfo.cpp 파일은 어셈블리 어트리뷰트를 정의하는 부분으로 다음과 같이 코드가 구성되어있습니다.

[#M_AssemblyInfo.cpp 보기|AssemblyInfo.cpp 닫기|

#include “stdafx.h”

using namespace System;
using namespace System::Reflection;
using namespace System::Runtime::CompilerServices;
using namespace System::Runtime::InteropServices;
using namespace System::Security::Permissions;

//
// 어셈블리의 일반 정보는 다음 특성 집합을 통해 제어됩니다.
// 어셈블리와 관련된 정보를 수정하려면
// 이 특성 값을 변경하십시오.
//
[assembly:AssemblyTitleAttribute(“CLRxASPX”)];
[assembly:AssemblyDescriptionAttribute(“”)];
[assembly:AssemblyConfigurationAttribute(“”)];
[assembly:AssemblyCompanyAttribute(“”)];
[assembly:AssemblyProductAttribute(“CLRxASPX”)];
[assembly:AssemblyCopyrightAttribute(“Copyright (c)  2009”)];
[assembly:AssemblyTrademarkAttribute(“”)];
[assembly:AssemblyCultureAttribute(“”)];

//
// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
//
//      주 버전
//      부 버전
//      빌드 번호
//      수정 버전
//
// 모든 값을 지정하거나 아래와 같이 ‘*’를 사용하여 빌드 번호 및 수정 버전이 자동으로
// 지정되도록 할 수 있습니다.

[assembly:AssemblyVersionAttribute(“1.0.*”)];

[assembly:ComVisible(false)];

[assembly:CLSCompliantAttribute(true)];

[assembly:SecurityPermission(SecurityAction::RequestMinimum, UnmanagedCode = true)];

_M#]

C#에서와 마찬가지인 어트리뷰트 사용법입니다. 다만 C++ 컴파일러의 특성상 空문장임을 뒷쪽에 명시해둔 것이 다른 부분이라고 할 수 있겠습니다. 그리고 네임스페이스의 참조 방법이 C#과 동일하게 되어있습니다. C#에서 흔히 사용하는 using은 C++의 using namespace와 같습니다. 그리고 C++에서 흔히 사용하는 using std::cout; 같은 참조는 C#에서는 using Console = System.Console; 과 같이 표현이 가능합니다.

특이한 점이 하나 더 있다면 stdafx.h의 존재입니다. MFC나 ATL 프로젝트에서 Precompiled Header라는 명목으로 많이 사용되던 것이 C++ CLR에도 그대로 적용된 걸 볼 수 있습니다. 거리낌없이 그 개념 그대로 stdafx.h에 Windows API 헤더를 추가하거나 프로젝트 전반에 걸쳐 사용하고 싶은 다른 라이브러리의 헤더를 추가하면 됩니다.

그럼 stdafx.h는 어떻게 구성되어있는지 한 번 살펴보도록 하겠습니다.

[#M_stdafx.h 보기|stdafx.h 닫기|

// stdafx.h : 자주 사용하지만 자주 변경되지는 않는
// 표준 시스템 포함 파일 및 프로젝트 관련 포함 파일이
// 들어 있는 포함 파일입니다.

#pragma once

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

_M#]

C++ CLR의 유용함이 느껴지는 부분입니다. 굵게 표시한 부분이 예제를 위하여 추가된 코드로, Win32 API 헤더를 추가한 것입니다. WIN32_LEAN_AND_MEAN은 컴파일러나 링커의 처리를 최소화하기 위한 것으로 통계적으로 사용 확률이 그닥 높지 않은 코드, 라이브러리를 사전에 제거해주는 옵션입니다. 예제에서는 API 참조를 위해서만 stdafx.h를 이용한 것이므로 stdafx.cpp에는 다른 서술이 되어있지 않아서 생략합니다.

이제 실제 코드를 살펴보기로 합니다.

[#M_CLRxASPX.h 보기|CLRxASPX.h 접기|

// CLRxASPX.h

#pragma once

using System::Object;
using System::EventArgs;
using System::Web::UI::Page;

namespace CLRxASPX {
public ref class MyCLRPage : public Page {
 protected:
void Page_Load(Object ^sender, EventArgs ^e);

};
}

_M#]

using namespace로 모든 네임스페이스의 요소들을 참조할 수도 있지만  필요한 항목들만 위와 같이 참조할 수도 있습니다. 그리고 아래의 클래스 선언을 보면 일반 C++ 클래스 선언과는 구별되는 내용들이 몇 가지 있습니다. 우선, class 키워드 앞에 ref 키워드가 사용된 걸 볼 수 있는데 이것이 이 클래스가 Managed Class 임을 나타내는 것입니다. 그 다음에는 상속 대상을 ASP.NET의 페이지 클래스로 정한 것이 보입니다.

Protected 멤버 메서드로 Page_Load 메서드가 선언된 것이 보입니다. 이 때 알아둘 것이 있는데 ASP.NET은 내부적으로 Reflection을 통해서 실제로 Page 클래스의 Load 이벤트에 추가한 이벤트 처리기가 아니라도 자동으로 이름을 통하여 호출 대상을 결정하기 때문에 위의 메서드는 항상 실행됩니다.

그리고 C++ CLR에서 닷넷 프레임워크의 형식들을 이용할 때에는 닷넷 프레임워크 상의 관리되는 참조 객체임을 표현하기 위하여 과거의 Managed Extensions for C++ 시절때 사용하였던 __gc * 형식 대신 ^ 기호를 사용합니다. 포인터의 * 와는 구분되는 것으로 주소값을 얻기 위한 형변환 시도는 허용되지 않습니다. 이렇게 사용되는 형변환은 주소값에 대한 형변환이 아니라 형식 자체에 대한 형변환 시도가 됩니다. 별도로 해당 형식이 암묵적/명시적 형변환 연산자를 가지고 있지 않을 경우 상속 관계와는 무관하므로 컴파일이 실패합니다. 참고로, Page_Load 메서드가 이벤트 핸들러로서의 조건을 충족하려면 반환 형식은 void, 즉 반환 대상이 없으며, 두 개의 매개 변수를 받아들여야 합니다. 하나는 이벤트를 발행한 출처의 참조, 또 하나는 이벤트 인자 개체의 참조입니다. 자세한 내용은 System::EventHandler 대리자를 참고하시면 됩니다.

[#M_CLRxASPX.cpp 보기|CLRxASPX.cpp 접기|

// 기본 DLL 파일입니다.

#include “stdafx.h”
#include “CLRxASPX.h”

using System::String;
using System::Object;
using System::IntPtr;
using System::EventArgs;
using System::Web::UI::WebControls::Label;

void CLRxASPX::MyCLRPage::Page_Load(Object ^sender, EventArgs ^e) {
 SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);

 Label ^oemIdentity = gcnew Label();
oemIdentity->Text = String::Format(L”OEM Identity: {0}<br />”, sysInfo.dwOemId);
this->Controls->Add(oemIdentity);

Label ^procArch = gcnew Label();
procArch->Text = L”Processor Architecture: “;

switch (sysInfo.wProcessorArchitecture) {
case PROCESSOR_ARCHITECTURE_AMD64:
procArch->Text += L”x64 (AMD or Intel)”;
break;
case PROCESSOR_ARCHITECTURE_IA64:
procArch->Text += L”Intel Itanium Processor Family (IPF)”;
break;
case PROCESSOR_ARCHITECTURE_INTEL:
procArch->Text += L”x86″;
break;
default:
procArch->Text += L”Unknown processor”;
break;
}

 procArch->Text += L”<br />”;
this->Controls->Add(procArch);

Label ^reservedWord = gcnew Label();
reservedWord->Text = String::Format(L”Reserved: {0}<br />”, sysInfo.wReserved);
this->Controls->Add(reservedWord);

Label ^pageSizeInfo = gcnew Label();
pageSizeInfo->Text = String::Format(L”Current page size: {0}<br />”, (int)sysInfo.dwPageSize);
this->Controls->Add(pageSizeInfo);

Label ^minAppAddress = gcnew Label();
IntPtr ^ptrValueMin = gcnew IntPtr(sysInfo.lpMinimumApplicationAddress);
minAppAddress->Text = String::Format(L”Minimum application address: {0:x}<br />”, ptrValueMin->ToInt32());
this->Controls->Add(minAppAddress);

Label ^maxAppAddress = gcnew Label();
IntPtr ^ptrValueMax = gcnew IntPtr(sysInfo.lpMaximumApplicationAddress);
maxAppAddress->Text = String::Format(L”Maximum application address: {0:x}<br />”, ptrValueMax->ToInt32());
this->Controls->Add(maxAppAddress);

Label ^activeProcMask = gcnew Label();
activeProcMask->Text = String::Format(L”Active processor mask: {0}<br />”, sysInfo.dwActiveProcessorMask);
this->Controls->Add(activeProcMask);

Label ^procCount = gcnew Label();
procCount->Text = String::Format(L”Number of processors: {0}<br />”, sysInfo.dwNumberOfProcessors);
this->Controls->Add(procCount);

Label ^procType = gcnew Label();
procType->Text = L”Processor Type: “;

switch (sysInfo.dwProcessorType) {
case PROCESSOR_INTEL_386:
procType->Text = String::Concat(procType->Text, L”Intel 386 Processor”);
break;
case PROCESSOR_INTEL_486:
procType->Text = String::Concat(procType->Text, L”Intel 486 Processor”);
break;
case PROCESSOR_INTEL_PENTIUM:
procType->Text = String::Concat(procType->Text, L”Intel Pentium Class Processor”);
break;
case PROCESSOR_INTEL_IA64:
procType->Text = String::Concat(procType->Text, L”Intel IA64 Processor”);
break;
case PROCESSOR_AMD_X8664:
procType->Text = String::Concat(procType->Text, L”Intel – or – AMD x86/64 Processor”);
break;
default:
procType->Text = String::Concat(procType->Text, L”Unknown processor type”);
break;
}

procType->Text = String::Concat(procType->Text, L”<br />”);
this->Controls->Add(procType);

Label ^allocGranularity = gcnew Label();
allocGranularity->Text = String::Format(
L”Virtual memory allocation granularity: {0}<br />”,
sysInfo.dwAllocationGranularity);
this->Controls->Add(allocGranularity);

Label ^procLevel = gcnew Label();
procLevel->Text = String::Format(L”Processor Level: {0}<br />”, sysInfo.wProcessorLevel);
this->Controls->Add(procLevel);

Label ^procRevision = gcnew Label();
procRevision->Text = String::Format(L”Processor Revision: {0}<br />”, sysInfo.wProcessorRevision);
this->Controls->Add(procRevision);
}

_M#]

구현하기 나름이지만, 원래의 C++ 코드 스타일을 유지하기 위하여 선언부와 구현부를 나눈다면 위와 같은 형태로 cpp 파일에 메서드 본문을 작성할 수 있습니다. 이 때, 메서드의 시그니처에 전체 네임스페이스가 지정되어야 함을 유의합니다.

windows.h 헤더 파일 내에 포함되어있는 항목들 중 시스템 정보를 추출하는 API를 이용해보기로 합니다. 이 부분은 C#이나 기타 닷넷 환경에서는 원래 Platform Invoke를 통하여 우회적으로 마샬링해야 하는 부분이지만 C++ CLR의 특성에 따라 Native Code를 평소처럼 실행할 수 있다는 것을 보여줍니다. SYSTEM_INFO 구조체의 주소값을 전달하여 시스템 정보를 구조체에 채우도록 만들고 이를 ASP.NET 서비스에서 보여준다는 것이 예제 프로그램의 컨셉입니다.

Label 컨트롤을 생성하기 위하여 gcnew 연산자를 사용하였습니다. 역시 C++에서 원래 사용하던 new 연산자와 구분되는 것입니다. gcnew 연산자로 관리되는 객체를 만들고, Text 프로퍼티에 문자열을 지정하였습니다. 관리 객체에 지정되는 문자열은 자동으로 System::String의 인스턴스로 분류되어 처리되도록 만들어져 있기 때문에 유니코드 문장임을 나타내는 접두사 L 없이도 쓰일 수 있습니다. 또한, 아랫쪽 코드를 보면 System::String 형식에만 정의되어있는 문자열간 합치기 기능이 C++에서도 동작하는 것을 볼 수 있습니다.

위의 예제 코드를 작성한 다음, 결과를 확인하기 위하여 아래와 같이 IIS 7.0에서 사용할 수 있는 web.config 파일을 작성하여 웹 어플리케이션 디렉터리를 구축하고, 해당 웹 어플리케이션 디렉터리 바로 아래의 Bin 폴더에 이 프로젝트로 빌드한 DLL을 저장하였습니다.

[#M_web.config 보기|web.config 닫기|<?xml version=”1.0″ encoding=”utf-8″?>
<configuration>
<system.webServer>
<handlers>
<add name=”MyCLRPage”
path=”Default.aspx”
type=”CLRxASPX.MyCLRPage, CLRxASPX”
verb=”GET,POST,HEAD”
preCondition=”integratedMode,runtimeVersionv2.0″ />
</handlers>
</system.webServer>
</configuration>_M#]

위의 내용에서 확인해야 할 부분이 두 가지가 있습니다. IIS 7의 system.webServer 섹션은 닷넷 프레임워크가 처리하는 것이 아니라 IIS 7이 스스로 처리하는 부분입니다. 우선 핸들러에 type이라고 되어있는 부분은 닷넷 프레임워크에서 불러들일 IHttpHandler 인터페이스와 호환되는 형식의 약식 명칭을 지정하는 부분입니다. Page 클래스를 상속함으로서 이 부분이 자동으로 구현되었기 때문에 우리가 방금 작성한 C++ CLR 클래스의 약식 명칭을 여기에 기술합니다. 네임스페이스와 클래스 이름, 그리고 Bin 폴더 내에 들어있는 클래스 라이브러리의 어셈블리 이름을 여기에 기출합니다. 그리고 preCondition에서는 이 핸들러가 통합 모드에서 실행되어야 하고 닷넷 프레임워크 2.0 런타임을 사용하여 구동되어야 함을 표기하고 있습니다.

이제 실행 결과를 살펴보도록 하겠습니다.

C++ CLR이 Native Code와 잘 연동되어 ASP.NET 서비스를 수행하고 있는 것을 볼 수 있습니다. 여기서는 공식적으로 프로젝트 템플릿이 제공된 것이 아니라서 이와 같이 간소한 형태로만 프로그램을 작성하였지만 C++ CLR로도 이와 같이 프로그램 작성을 할 수 있다는 것을 확인할 수 있었습니다. Platform Invoke로 해결하기 어려운 문제는 이와 같이 프로그램의 형태에 제한을 갖지 않고 직접 C++의 힘을 빌릴 수도 있다는 것을 기억하면 쉽게 문제를 풀어나갈 수 있을 것입니다.

참고로 C++/CLI의 ECMA 표준안 문서는 http://www.ecma-international.org/publications/standards/Ecma-372.htm 에서 확인가능합니다.

댓글 남기기