.NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기
전에 쓴 글에서,
.NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요?
; https://www.sysnet.pe.kr/2/0/1659
"대신... 이런 방법은 어떨까요? Visual C++로 exe를 만들고 그 안에 닷넷 EXE + app.config을 리소스로 포함하는 것입니다" 라고 잠깐 언급을 했었는데요. 생각해 보니 app.config을 포함할 필요는 없습니다. 즉, app.config없이 다음과 같은 supportedRuntime이 설정된 것처럼 동작시킬 수 있습니다.
<?xml version="1.0"?>
<configuration>
<startup>
<supportedRuntime version="v4.0.30319"/>
<supportedRuntime version="v2.0.50727"/>
</startup>
</configuration>
왜냐하면 이를 위한 2가지 조건이 이미 갖춰져 있기 때문입니다.
- C++에서는 원하는 CLR을 로드할 수 있다.
- CLR 4 환경에서도 .NET 2.0 대상의 어셈블리를 로드할 수 있다.
자.. 그럼 실제로 한번 해볼까요? ^^
호스팅 코드에 대해서는 이미 다음과 같이 CLR 2/4에 대해 각각 마이크로소프트에서 친절하게 예제 코드로 배포하고 있으니 이를 참조하겠습니다.
C++ app hosts CLR and invokes .NET assembly (CppHostCLR)
; http://code.msdn.microsoft.com/windowsdesktop/CppHostCLR-4da36165
C++ app hosts CLR 4 and invokes .NET assembly (CppHostCLR)
; http://code.msdn.microsoft.com/windowsdesktop/CppHostCLR-e6581ee0
우선, CLR 4 환경을 로드하는 시도를 해보겠습니다.
HRESULT hr;
ICLRMetaHost *pMetaHost = nullptr;
hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&pMetaHost));
ICLRMetaHost 인터페이스는 .NET 4.0부터 지원하고 있기 때문에 .NET 2.0만 설치된 컴퓨터에서는 인스턴스 생성에 실패하게 됩니다. 오호~~~ 그렇다면 이것에 실패했을 때는 재차 CLR 2 환경을 초기화하려는 시도를 해보면 된다는 이야기입니다.
ICorRuntimeHost *pCorRuntimeHost = nullptr;
if (FAILED(hr))
{
wprintf(L"CLRCreateInstance failed: 0x%x (.NET 4 not installed)\n", hr);
// 실패한다면 CLR 2 로드를 시도
PCWSTR pszFlavor = L"wks";
PCWSTR pszVersion = L"v2.0.50727";
hr = CorBindToRuntimeEx(
pszVersion, // Runtime version
pszFlavor, // Flavor of the runtime to request
0, // Runtime startup flags
CLSID_CorRuntimeHost, // CLSID of ICorRuntimeHost
IID_PPV_ARGS(&pCorRuntimeHost) // Return ICorRuntimeHost
);
if (FAILED(hr))
{
wprintf(L".NET 2.0 load failed\n");
break;
}
else
{
wprintf(L".NET 2.0 loaded\n");
}
}
위의 코드에서 ICorRuntimeHost 인터페이스를 구해오는데요. .NET 4.0 로드에서 성공했다면 ICLRMetaHost로부터 다음과 같은 절차를 거쳐서 동일한 ICorRuntimeHost 인터페이스를 구할 수 있습니다.
hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&pRuntimeInfo));
if (FAILED(hr))
{
wprintf(L".NET 4.0 load failed\n");
break;
}
else
{
wprintf(L".NET 4.0 loaded\n");
}
hr = pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_PPV_ARGS(&pCorRuntimeHost));
if (FAILED(hr))
{
wprintf(L"GetInterface(CLSID_CorRuntimeHost) failed\n");
break;
}
여기까지 되었으면 게임 끝입니다. 컴퓨터에 설치된 CLR을 로드했으므로 Default AppDomain을 구하고,
hr = pCorRuntimeHost->Start(); // CLR 구동
if (FAILED(hr))
{
wprintf(L"CLR failed to start\n");
break;
}
{
IUnknownPtr spAppDomainThunk = nullptr;
_AssemblyPtr spAssembly = nullptr;
hr = pCorRuntimeHost->GetDefaultDomain(&spAppDomainThunk);
if (FAILED(hr))
{
wprintf(L"GetDefaultDomain failed\n");
break;
}
_AppDomainPtr spDefaultAppDomain = spAppDomainThunk;
if (spDefaultAppDomain == nullptr)
{
wprintf(L"Failed to get default _AppDomainPtr\n");
break;
}
// AppDomain을 구했으므로 어셈블리를 로드하고, 실행하는 코드를 추가
// ... [생략: 아래에서 설명]...
}
pCorRuntimeHost->Stop();
그 중간에 우리가 실행할 .NET EXE를 로드하고 실행하는 코드를 추가하면 됩니다. 우리가 의도하는 것은 .NET 2.0/4.0에 상관없이 실행할 수 있는 단일 EXE 파일만 배포하는 것이므로 .NET 2.0 대상으로 다음의 코드를 포함하는 C# 프로젝트를 하나 만들고,
using System;
class Program
{
static void Main()
{
Console.WriteLine("Sample .NET App - running...");
}
}
컴파일된 EXE 파일을 C++ 프로젝트에 리소스로 포함시킵니다. 포함된 리소스 바이너리는 다음과 같이 구할 수 있고,
HMODULE hModule = ::GetModuleHandle(NULL);
HRSRC hResource = FindResource(hModule, MAKEINTRESOURCE(IDR_EXEFILE1), L"EXEFILE");
HGLOBAL hMemory = LoadResource(hModule, hResource);
DWORD dwSize = SizeofResource(hModule, hResource);
LPVOID lpAddress = LockResource(hMemory);
이를 SafeArray에 담아 Main 메서드를 찾아 실행하면 됩니다.
SAFEARRAYBOUND rgsabound[] = { dwSize, 0 };
SAFEARRAY *pSafeArray = SafeArrayCreate(VT_UI1, 1, rgsabound);
pSafeArray->pvData = lpAddress;
hr = spDefaultAppDomain->Load_3(pSafeArray, &spAssembly);
/*
Critical error detected c0000374
ConsoleApplication1.exe has triggered a breakpoint.
*/
// SafeArrayDestroy(pSafeArray);
if (FAILED(hr))
{
wprintf(L"Failed to load the assembly\n");
break;
}
_MethodInfoPtr mainMethod;
hr = spAssembly->get_EntryPoint(&mainMethod);
if (FAILED(hr))
{
wprintf(L"No Entry method\n");
break;
}
VARIANT vtEmpty;
VariantInit(&vtEmpty);
BindingFlags flags = (BindingFlags)(BindingFlags::BindingFlags_InvokeMethod | BindingFlags::BindingFlags_Static);
hr = mainMethod->Invoke_2(vtEmpty, flags,
nullptr, nullptr, nullptr, nullptr);
생각보다 어렵지 않지요? ^^
이제 C++ 프로젝트를 빌드한 단일 EXE 파일을 .NET 2.0만 설치된 컴퓨터에 복사해서 실행하면 다음과 같은 출력 결과를 얻을 수 있고,
D:\temp>ConsoleApplication1.exe
CLRCreateInstance failed: 0x80004001 (.NET 4 not installed)
.NET 2.0 loaded
Sample .NET App - running...
.NET 4.0만 설치된 컴퓨터에서도 마찬가지로 잘 실행이 되는 것을 확인할 수 있습니다.
D:\temp>ConsoleApplication1.exe
.NET 4.0 loaded
Sample .NET App - running...
오~~~ 멋집니다. ^^
게다가 .NET Framework이 아예 설치되어 있지 않다면 이후의 원하는 동작도 자유롭게 제어할 수 있는 권한도 얻게 된 것입니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
(
github에 예제 코드를 올려 두었습니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]