오늘 회사에서 아는 형이 코드에서 "PInvokeStackImbalance" Exception이 발생한다고 도와달라고 했다.

사실 여기서 가장 큰 문제는 "왜 되던 코드가 안되는 것일까?" 였다. 이전 프로젝트에서는 DLL을 잘 로드해서 썼는데, 새로 프로젝트를 만들고 똑같이 DLL을 로드했는데 갑자기 위의 Exception이 발생했다고 한다.

그래서 두 프로젝트를 비교해봤더니 사용하던 .Net Framework 버전이 달랐다. 잘 되던 곳은 3.0을 쓰고 있었고, 안되는 곳은 4.0을 쓰는 문제가 있었다.

그래서 찾아봤다. 역시 문제는 CallingConvention 문제.

[DllImport("TestDll.dll")]
public static extern int Test(string text);

위와 같이 작성되었던 것을

[DllImport("TestDll.dll", CallingConvention=CallingConvention.Cdecl)]
public static extern int Test(string text);

이렇게 뒤에 CallingConvention을 붙여서 호출하면 된다.


보통 C++에서 DLL을 만들면 cdecl 이란 호출규약으로 만든다. 왜냐면 다들 귀찮으니까 별도의 호출규약을 안써준다. 근데, C#에서 DllImportAttributeCallingConvention은 기본값이 stdcall이다. 그래서 에러가 난거다.


호출규약에 대해서 궁금하면 여기! 를 참고하면 좋다.


뭐... 이거야 쉽게 해결했지만 왜 3.0에서는 된걸까?

여러 글을 찾아보다가 결국 해답은 못찾았다. 하지만 StackOverflow에 같은 질문이 올라온 글에 David Hefferman이란 사람이 아래와 같이 적어놓았다.

Don't kid yourself that your code is alright because .net 3.5 doesn't raise this error. The error detection in .net 4 is better which is why you only see the errors there. But your code is broken in all .net versions.

굳이 해석하자면...

"3.5에서 에러가 나지 않았다고 코드에 이상없다고 착각하지 마. 단지 4.0에서 이러한 에러를 잘 찾아내니까 발생한거야. 니 코드는 모든 .net 버전에서 에러야"


음. 명언이다. 에러가 안난건 단지 4.0의 성능이 좋아서다. 내가 봐도 확실히 에러 맞다.



posted by 스펜서.

VC++ DLL 자동 버전 빌드 구성하기

Source Code 2010. 8. 6. 20:28

들어가면서……

여러 모듈로 나누어진 프로젝트의 각 모듈 담당자라면 누구나 한번쯤은 DLL의 버전관리가 안돼서 곤란한 경우가 있을 것이다. 언제 빌드한 DLL인지 헷갈리곤 한다. 그렇다고 매번 빌드해서 커밋할 때 마다 Version 리소스를 수정하는 것도 귀찮고, 까먹으면 곤란한 일이 꼭 발생한다. 그래서 이 아티클에서는 자동으로 DLL의 버전을 수정하는 방법을 소개한다. 이 아티클에서는 Visual Studio 2005를 기준으로 설명하겠다.

 

시작!!

Auto Versioning Build는 다음과 같은 단계로 이루어진다.

1. [빌드 전] Version을 정의한 파일을 Update한다.

2. [빌드 중] 정의된 Version을 리소스 파일에 반영한다.

생각보다 단순하다. 지금부터 1번과 2번을 나누어 설명하도록 하겠다.

 

1. [빌드 전] Version을 정의한 파일을 Update한다.

2번을 가능하게 하려면, Version #define 구문을 이용해서 정의해야 한다. 필자는 다음과 같이 정의했다.

 

#define FILE_VERSION    3,0,806,2

#define PRODUCT_VERSION    3,0,0,0

#define STR_FILE_VERSION    "3,0,806,2\0"

#define STR_PRODUCT_VERSION    "3,0,0,0\0"

 

버전을 나타내는 문자열은 다양한 것들이 있지만, 주로 빈번히 바뀔 파일 버전과 제품 버전만 정의하였다. 그리고 “STR_”로 시작하는 선언은 문자열 형태로 버전을 정의한 항목이다. 나중에 리소스파일을 열어보면 문자열로 주어야 하는 항목이 있기 때문이다.

이와 같이 작성한 파일을 [VersionInfo.h]하고 소스가 있는 프로젝트 폴더에 저장했다.

 

그 다음은 이 [VersionInfo.h]를 업데이트할 프로그램을 구현해야 한다. 필자는 버전을 다음과 같은 버전 정책을 사용하였다.

  • 제품 버전의 맨 뒤, 두 자리는 0으로 고정.

  • 제품 버전의 앞 두 자리는 파일 버전의 앞 두 자리와 동일

  • 파일 버전의 세 번째 자리는 빌드 날짜

  • 파일 버전의 네 번째 자리는 해당 날짜에 빌드한 횟수


예를 들어, 제품 버전이 3.0.0.0이고 오늘 날짜가 8 6일이고, 빌드 횟수가 19이면 파일 버전은 3.0.806.19가 되는 것이다. 버전 정책은 사람마다, 회사마다 다르니까 입맛에 맞게 정책을 세우길 바란다.

 

이제, 본인이 세운 버전 정책에 맞게 [VersionInfo.h]를 업데이트 할 프로그램을 구현해야 한다. DLL의 버전 정책을 고민할 개발자라면 이 정도는 구현할 거라 믿는다. [Annex A]에 필자가 구현한 소스를 첨부한다. 참고하실 분은 참고하길 바란다(매우 단순 무식하게 구현하였음). 필자는 제품 버전과 파일 버전을 먼저 파싱하고, 이를 바탕으로 다음 버전을 업데이트 하도록 하였다. 그래서 개발자가 제품 버전의 앞 두 버전을 수정하면 이것이 자연스럽게 파일 버전에 적용되고, 빌드 횟수는 파일 버전의 맨 뒤 버전 +1을 하여 구하도록 하였다. 만약, 날짜를 나타내는 세 번째 버전 필드가 오늘 날짜와 다르면 네 번째 버전 필드를 1로 초기화 하도록 하였다.

 

다음은 [VersionInfo.h]가 버전 리소스에 참조가 되게끔 수정해보자.

 

2. [빌드 중] 정의된 Version을 리소스 파일에 반영한다.

Visual Studio 2005로 개발하면 아마 [(프로젝트명).rc]파일과 [res\(프로젝트명).rc2]라는 파일을 봤을 것이다. 두 파일 모두 리소스를 정의하는 파일이지만 열어보면 많이 다른 것을 알 수 있다.

먼저 [(프로젝트명).rc]를 살펴보자.

 

// Microsoft Visual C++ generated resource script.

//

#include "resource.h"

 

#define APSTUDIO_READONLY_SYMBOLS

////////////////////////////////////////////////////////////////////////////

//

// Generated from the TEXTINCLUDE 2 resource.

//

#include "afxres.h"

(중간 생략)

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_KOR)

#ifdef _WIN32

LANGUAGE 18, 1

#pragma code_page(949)

#endif //_WIN32

#include "res\MMLayer.rc2"     // non-Microsoft Visual C++ edited resources

#include "l.kor\afxres.rc"     // Standard components

#endif

 

///////////////////////////////////////////////////////////////////////////

#endif    // not APSTUDIO_INVOKED

 

아마 파일이 이와 다를 수 있다. 어쨌든 맨 위를 보면 자동으로 생성되는 스크립트임을 나타내는 주석이 달려있다. 아래를 보니 “non-Microsoft Visual C++ edited resources” 라는 주석이 [res\(프로젝트명).rc2]에 달려 있는 것을 볼 수 있다. rc파일은 IDE에 의해서 자동으로 생성되는 파일이므로 수정하면 안되고, 개발자가 원하는 리소스는 rc2파일에 기록하는 것임을 알 수 있다. 그럼 이제 [res\(프로젝트명).rc2]를 열어보자.

 

//

// MMLAYER.RC2 - resources Microsoft Visual C++ does not edit directly

//

#ifdef APSTUDIO_INVOKED

    #error this file is not editable by Microsoft Visual C++

#endif //APSTUDIO_INVOKED

 

//////////////////////////////////////////////////////////////////////////

// Add manually edited resources here...

//////////////////////////////////////////////////////////////////////////

 

빙고!! 수동으로 리소스를 추가하려면 여기에 하라고 써있다. 그럼 이제 여기에 Version 리소스를 추가해보자. 가장 쉬운 방법은 rc파일에 있는 VS_VERSION_INFO로 시작하는 섹션을 복사/붙여넣기로 추가하거나 아래 코드를 복사하면 된다.

 

VS_VERSION_INFO VERSIONINFO

 FILEVERSION 1.0.0.0

 PRODUCTVERSION 1.0.0.0

 FILEFLAGSMASK 0x17L

#ifdef _DEBUG

 FILEFLAGS 0x1L

#else

 FILEFLAGS 0x0L

#endif

 FILEOS 0x4L

 FILETYPE 0x2L

 FILESUBTYPE 0x0L

BEGIN

    BLOCK "StringFileInfo"

    BEGIN

        BLOCK "041204b0"

        BEGIN

            VALUE "CompanyName", "Medison"

            VALUE "FileDescription", "Version"

            VALUE "FileVersion", “1.0.0.0”

            VALUE "InternalName", "MMLayer.dll"

            VALUE "LegalCopyright", "Copyright (C) 2010"

            VALUE "OriginalFilename", "MMLayer.dll"

            VALUE "ProductName", "Medison Ultrasound Equipment"

            VALUE "ProductVersion", “1.0.0.0”

        END

    END

    BLOCK "VarFileInfo"

    BEGIN

        VALUE "Translation", 0x412, 1200

    END

END

 

천천히 읽어보면 대충 감이 올꺼다. 회사 이름, 파일 설명, 파일 버전, 내부 이름, 회사 이름 등등 IDE에서 우리가 삽입했던 이름들이다. 여기서 우리가 눈 여겨 봐야 할 부분은 아래 4가지이다.

  • FILEVERSION

  • PRODUCTVERSION

  • “FileVersion”

  • “ProductVersion”

이제 위 항목에 1.0.0.0으로 되어 있는 부분을 우리가 정의한 FILE_VERSION, PRODUCT_VERSION, STR_FILE_VERSION, STR_PRODUCT_VERSION을 대입해 주면 된다.

 

어떻게 하면 좋을까? 눈치 빠른 사람이라면 알 것이다. 자세히 보면, C++에 있는 전처리 명령어들이 있는 것을 볼 수 있다. 즉 맨 위에 우리가 만든 [VersionInfo.h] Include하고 1.0.0.0으로 되어 있는 부분을 적절한 define 항목으로 바꾸면 되는 것이다. 아래는 필자의 rc2파일이다.

//

// MMLAYER.RC2 - resources Microsoft Visual C++ does not edit directly

//

#include "..\\VersionInfo.h"

 

#ifdef APSTUDIO_INVOKED

    #error this file is not editable by Microsoft Visual C++

#endif //APSTUDIO_INVOKED

 

 

/////////////////////////////////////////////////////////////////////////////

// Add manually edited resources here...

/////////////////////////////////////////////////////////////////////////////

VS_VERSION_INFO VERSIONINFO

 FILEVERSION FILE_VERSION

 PRODUCTVERSION PRODUCT_VERSION

 FILEFLAGSMASK 0x17L

#ifdef _DEBUG

 FILEFLAGS 0x1L

#else

 FILEFLAGS 0x0L

#endif

 FILEOS 0x4L

 FILETYPE 0x2L

 FILESUBTYPE 0x0L

BEGIN

    BLOCK "StringFileInfo"

    BEGIN

        BLOCK "041204b0"

        BEGIN

            VALUE "CompanyName", "Medison"

            VALUE "FileDescription", "Version"

            VALUE "FileVersion", STR_FILE_VERSION

            VALUE "InternalName", "MMLayer.dll"

            VALUE "LegalCopyright", "Copyright (C) 2010"

            VALUE "OriginalFilename", "MMLayer.dll"

            VALUE "ProductName", "Medison Ultrasound Equipment"

            VALUE "ProductVersion", STR_FILE_VERSION

        END

    END

    BLOCK "VarFileInfo"

    BEGIN

        VALUE "Translation", 0x412, 1200

    END

END

빨간색 부분이 필자가 수정한 부분이다. 이렇게 하면 리소스 파일을 컴파일 할 때, [VersionInfo.h] 파일을 참조해서 컴파일하게 된다.

 

3. IDE 설정하기

여기까지 했으면, 거의 다 했다. 이제, 빌드 하기 전에 [VersionInfo.h]를 업데이트하도록 우리가 1번에서 구현한 프로그램을 실행시키게끔 IDE에 설정하면 된다.

프로젝트 > 속성 > 구성속성 > 빌드 이벤트에 보면 세가지 이벤트가 있다.

  • 빌드 전 이벤트

  • 링크 전 이벤트

  • 빌드 후 이벤트

각각은 이름만 봐도 언제 일어나는 이벤트인지 알 수 있을 것이다. 각각의 이벤트를 눌러보면 [명령줄]이란 항목이 있는데, 여기에 우리가 개발한 프로그램을 등록시키면 간단히 해결된다. 필자는 [DllVersion.exe]이라고 프로그램을 구현했으며, 소스가 있는 폴더에 함께 넣어놓았다. 다음은 설정하는 곳의 스크린샷이다.

 

 

필자는 Release로 빌드 했을 때만 버전 정보를 업데이트 하기 위해서 Release [빌드 전 이벤트]에 추가하였다. 이렇게 설정하고 빌드하면 자동으로 [VersionInfo.h]를 업데이트하고 빌드를 시작하게 된다.

 

끝내며……

비록 중간에 파일을 파싱하고 다시 출력하는 프로그램을 구현해야 하는 귀찮음이 있지만(물론 Annex A의 소스를 가져가면 덜 하겠지만…), 한번 해 놓으면 나중에 발생할 고생을 덜 하게 될 것이다. 한번쯤 해보면, 쉽게 다른 프로젝트에 적용 가능하니 시간 내서 해 보기를 권장한다.


Annex A

 

#include <stdio.h>

#include <time.h>

 

int main()

{

    FILE* version = NULL;

    fopen_s(&version, "VersionInfo.h", "r");

 

    if(version == NULL)

    {

        printf("File is not found. : VersionInfo.h\n");

        return -1;

    }

 

    char bufFileVer[1024];

    char bufProductVer[1024];

    char bufStrFileVer[1024];

    char bufStrProductVer[1024];

 

    fgets(bufFileVer, 1024, version);

    fgets(bufProductVer, 1024, version);

    fclose(version);

   

    int fileVer[4] = {0, };

    int productVer[4] = {0, };

 

    sscanf_s(bufFileVer, "#define FILE_VERSION\t%d,%d,%d,%d",

      &fileVer[0], &fileVer[1], &fileVer[2], &fileVer[3]);

       sscanf_s(bufProductVer, "#define PRODUCT_VERSION\t%d,%d,%d,%d",

         &productVer[0], &productVer[1], &productVer[2], &productVer[3]);

 

    fileVer[0] = productVer[0];

    fileVer[1] = productVer[1];

    productVer[2] = 0;

    productVer[3] = 0;

 

    tm today;

    time_t now;

    time(&now);

    _localtime64_s(&today, &now);

 

    int day = (today.tm_mon + 1) * 100 + today.tm_mday;

 

    if(day != fileVer[2])

    {

        fileVer[2] = day;

        fileVer[3] = 1;

    }

    else

    {

        fileVer[3]++;

    }

    printf(" \n \n");

    printf("= MMLayer Version. =====================\n");

    printf("   File    Ver.%5d,%5d,%5d,%5d\n",

      fileVer[0], fileVer[1], fileVer[2], fileVer[3]);

    printf("   Product Ver.%5d,%5d,%5d,%5d\n",

      productVer[0], productVer[1], productVer[2], productVer[3]);

    printf("========================================\n");

    printf(" \n \n");

 

   

    fopen_s(&version, "VersionInfo.h", "w+");

 

    if(version == NULL)

    {

        printf("File is not found. : VersionInfo.h\n");

        return -1;

    }

 

    sprintf_s(bufFileVer, "#define FILE_VERSION\t%d,%d,%d,%d\n",

      fileVer[0], fileVer[1], fileVer[2], fileVer[3]);

    sprintf_s(bufStrFileVer, "#define STR_FILE_VERSION\t\"%d,%d,%d,%d\\0\"\n",

      fileVer[0], fileVer[1], fileVer[2], fileVer[3]);

 

    sprintf_s(bufProductVer, "#define PRODUCT_VERSION\t%d,%d,%d,%d\n",

      productVer[0], productVer[1], productVer[2], productVer[3]);

    sprintf_s(bufStrProductVer, "#define STR_PRODUCT_VERSION\t\"%d, %d,%d,%d\\0\"\n",

      productVer[0], productVer[1], productVer[2], productVer[3]);

 

    fprintf_s(version, "%s", bufFileVer);

    fprintf_s(version, "%s", bufProductVer);

    fprintf_s(version, "%s", bufStrFileVer);

    fprintf_s(version, "%s", bufStrProductVer);

 

    fclose(version);

   

    return 0;

}

 

 

posted by 스펜서.