관리 메뉴

Kim's Programming

WinAPI - 시작하기전에 알고 갑시다. 본문

Programming/Windows API

WinAPI - 시작하기전에 알고 갑시다.

Programmer. 2015. 8. 24. 15:43

1. 변수 명명법


윈도우즈 프로그래밍에서는 도스(DOS)에서 와는 다릅니다. 도스에서는 변수가 i,j등 짧고 소문자만 이용하지만 윈도우즈에서는 많은 변수가 사용되므로 보통은 변수의 이름을 길게 쓰고 읽기 쉽게 하기 위해 대소문자를 혼합하여 사용합니다. 사용하는 변수가 많기 떄문에 단축형으로 쓰다보면 헷갈리기 쉽고 너무 길면 입력이 불편하기 때문에 잘 절충해야합니다. 특히 자주 사용하는 변수는 관습적으로 정해진 접두어를 사용하는데 다음은 윈도우즈 프로그래밍에서 주로 사용되는 접두어 들입니다.

 접두어 

 본딧말

 의미

 cb

 Count of Byte

 바이트 수

 dw

 double word

 부호없는 Long형 정수

 h

 handle

 윈도우, 비트맵, 파일 등의 핸들

 sz

 Null Terminated

 NULL 종료 문자열

 ch

 Character

 문자형

 a

 Array

 배열

 w

 Word

 부호없는 정수형

 i

 Integer

 정수형

 p,lp

 long pointer

 포인터형

 b

 Bool

 논리형


표준함수들은 모두 이 방식대로 인수 이름을 작성하므로 접두어들을 정확하게 외우고 있어야 함수의 인수가 어떤 의미인가를 금방 파악할 수 있습니다. 암기하기도 편하게 되어 있는데 예를들어서 cbString이라고 되있으면 문자열의 바이트 수를 나타내는 정수형 인수(또는 변수)라는 것을 알 수 있으며 szMessage라면 널 문자열을 가리키는 포인터라는 것을 유추할 수 있습니다. 윈도우즈 프로그래밍에서 쓰이는 이런 변수 명명법을 헝가리식 명명법(Hungarian Naming)이라고도 하는데 이렇게 하라! 라는 거 보다는 이렇게 하면 편하다! 라는 것입니다. 더구나 이 방법은 변수의 타입에 대한 정보가 이름에 포함되어 있어서 타입을 수정할 때 변수의 이름까지 같이 바꿔야 하기 때문에 잘 사용 안되기도 합니다. 하지만 반대로 생각하면 소스를 읽을 때 변수 이름만으로 타입 판별이 가능하여 도움이 됩니다. 다음 함수의 원형을 본다면 hWnd는 핸들이고 lpRect는 포인터 bErase는 진위형이라는 것을 바로 알 수 있습니다.
BOOL InvalidateRect(HWND hWnd, CONST RECT* lpRECT, BOOL bERASE);

변수명을 나타내는 위의 관습 말고도 윈도우즈에서는 사용자 정의 데이터형이 많이 있습니다. 이 데이터형은 windows.h안에 typedef로 선언되어 있으며 표준데이터형 처럼 사용하므로 알아두면 편합니다.

데이터형 

 의미

 BYTE

 unsigned char형

 CHAR

 char형

 WORD

 unsigned short형

 DWORD

 unsigned long형

 LONG

 long과 동일

 BOOL

 BOOL 정수형이며 TRUE,FALSE중 한 한값을 가짐


2. 핸들에 대한 이해

핸들(Handle)이란 구체적인 어떤 대상에 붙여진 번호이며 문법적으로는 32비트(또는 64비트)의 정수값입니다. 도스 프로그래밍에서는 거의 유일하게 저수준 파일 입출력시의 파일 핸들만이 사용되었으며 그렇기 때문에 도스애서의 핸들은 파일 핸들을 의미 하는 경우가 많았습니다. 그러나 윈도우즈에서는 여러 가지 종류의 핸들이 사용되고 있습니다. 만들어진 윈도우에는 윈도우 핸드(hWnd)을 붙여 번호로 관리하며 DC에 대해서도 핸들을 사용하고 논리적 펜, 브러시에도 핸들을 붙여 관리합니다. 심지어 메모리를 할당할 때도 할당한 메모리의 번지를 취급하기보다는 메모리에 번호를 붙인 메모리 핸들을 사용하기도 했습니다. 왜 이렇게 핸들을 자주 사용하는가 하면 대상끼리의 구분을 위해서는 문자열보다 정수형을 사용하는것이 더 빠르고 간편하기 떄문입니다. 윈도우즈에서 핸들을 이렇게 많이 사용하므로 우리는 핸들의 특성에 관해서 미리 숙지하는 것이 좋습니다. 핸들의 일반적인 특징입니다.


    1. 핸들은 우선 정수값이며 대부분의 경우 32비트 값입니다. 핸들을 사용하는 목적은 오로지 구분을 위한 것이므로 핸들끼리 중복되지 않아야 하며 이런 목적으로는 정수형이 가장 적합합니다. 정수형은 비교나 대입 연산이 가장 빠른 타입이므로 핸들로 쓰기에 적합합니다.

    2. 핸들은 운영체제가 발급하며 사용자는 쓰기만 합니다. 예를 들어 윈도우를 만들거나 파일을 열면 운영체제는 만들어진 윈도우나 열려진 파일에 핸들을 붙이고 그 값을 리턴합니다. 사용자는 이 핸들을 잘 보관해 두었다가 해당 윈도우나 파일을 다시 참조할 때 사용합니다. 사용자가 직접 핸들을 만드는 경우는 없습니다.

    3. 같은 종류의 핸들끼리는 절대로 중복된 값을 가지지 않습니다. 만약 이렇게 된다면 대상을 구분하는 본래의 목적을 달성할 수 없을 것입니다. 물론 다른 종류의 핸들끼리는 중복된 값을 가질 수도 있습니다.

    4. 핸들은 정수형이므로 값을 가지겠지만 그 실제값은 몰라도 상관이 없습니다. 핸들은 크고 작음보다는 단순한 표식입니다. 핸들형 변수를 만들어 핸들을 대입 받아 쓰고 난후에는 버리면 됩니다.

윈도우즈에서는 핸들은 예외없이 H로 시작되며 핸들값을 저장하기 위해 별도의 데이터형까지 정의해 두고 있습니다. HWND, HPEN, HBRUSH, HDC 등이 핸들에 담기 위한 데이터형들이며 모두 부호없는 정수형입니다.

3. 비트 OR 연산자


사용자는 원하는 작업을 하기 위해 함수를 호출합니다. 이때 함수에게 작업의 내용을 전달하는 인수가 같이 건네지는데 인수에는 작업의 목적과 방법을 지정하는 정보가 포함됩니다. 예를 들어 printf(result is %d",num); 이라는 함수 호출문에서 출력할 문자열의 서식과 서식의 해석방법을 두개의 인수로 전달했으며 printf함수는 이 인수값을 참조하여 사용자가 원하는 결과를 출력합니다. 윈도우즈 API 함수들도 마찬가지로 작업에 대한 세부정보(옵션)을 인수로 전달 받습니다. 그런데 전달 가능한 옵션이 여러 개 있을 경우 필요한 옵션수만큼 인수를 전달받는 것이 아니라 하나의 인수에 복수 개의 옵션을 묶어서 전달하는데 이 때 사용되는 연산자가 비트 OR(|)연산자 입니다.

도스에서는 이런 방식으로 옵션을 전달하는 경우가 fopen 등 극소수의 함수뿐이지만 윈도우즈에서는 아주 빈번합니다. 예를 들어서 CreateWindow함수는 만들고자 하는 윈도우의 스타일을 인수로 전달받는데 WS_CHILE, WS_CAPTION, WS_BORDER 등 옵션이 수십 개나 되지만 단 하나의 인수로 이 옵션들을 모두 전달받습니다. 만약 각각의 스타일을 개별 인수로 받는 다면 이 함수는 엄청난 수의 인수를 가져야 할 것입니다. 뿐만 아니라 호출할 때는 CreateWindow(NULL,0,FALSE,0,0,0,0,...)처럼 관심없는 인수에도 일일이 값을 지정해야 하므로 불편하고 스타일의 순서를 정확하게 외우는 것도 보통 일이 아닙니다. 비트 OR연산자를 사용하면 관심없는 인수는 생략해 버릴 수 있고 OR연산은 교환법칙이 성립하므로 값의 순서에 무관하며 여러 가지 옵션을 묶어서 전달하므로 호출 속도도 빠릅니다. DrawText함수는 문자열 출력 방법을 하나의 인수로 전달받는데 역시 옵션의 수가 무척 많습니다. Winuser.h파일을 보면 이 함수의 옵션 목록을 살펴볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define DT_TOP                      0x00000000
#define DT_LEFT                     0x00000000
#define DT_CENTER                   0x00000001
#define DT_RIGHT                    0x00000002
#define DT_VCENTER                  0x00000004
#define DT_BOTTOM                   0x00000008
#define DT_WORDBREAK                0x00000010
#define DT_SINGLELINE               0x00000020
#define DT_EXPANDTABS               0x00000040
#define DT_TABSTOP                              0x00000080
#define DT_NOCLIP                                 0x00000100
#define DT_EXTERNALLEADING           0x00000200
#define DT_CALCRECT                        0x00000400
#define DT_NOPREFIX                           0x00000800
#define DT_INTERNAL                           0x00001000
cs

수평으로 정렬을 하는 옵션, 수직 정렬을 하는 옵션, 개행 여부, 여러줄 출력 등의 옵션들이 있는데 이 많은 옵션들이 OR연산자를 통해 연결되어 딱 하나의 인수로 전달됩니다. 예를 들어 DT_CENTER | DT_BOTTOM | DT_WORDBREAK 등과 같이 하면 세 가지 옵션을 하나의 인수로 전달 한 것입니다. 한 인수에 이렇게 많은 옵션을 같이 전달할 수 있는 이유는 각 옵션이 비트별로 배정되어 있어서 상호 간섭하지 않게 됩니다. 이 옵션들의 실제값을 보면 1,2,4,8,등과 같이 2의 거듭승으로 정의되어 있어서 옵션별로 비트 자리가 정해져있습니다. 그래서 여러 개의 옵션을 비트 OR연산자로 묶어 하나의 정수값만 전달하더라도 함수는 인수의 개별 비트를 테스트한 후 어떤 옵션이 선택되었는지를 알 수 있습니다. 갑 옵션의 실제 값이 어떻게 정의 되어 있는가는 알 필요가 없으며 매크로 상수만 알면 되는데 매크로 상수가 곧 옵션의 이름입니다. 비트 OR연산자를 잘 알고 있는 사람에게는 너무 당연하게 생각되겠지만 이런 형태의 인수 전달을 처음 보는 사람에게는 무척 이상하게 보일 것입니다. 윈도우즈 API 함수는 이런 식의 ㅎ인수 전달법을 흔히 사용하며 때로는 별 상관없는 옵션들이 하나의 인수에 전달되는 경우도 있고 옵션끼리 상호 배타적인 경우도 있습니다.

3. 유니코드

유니코드는 16비트의 단일한 값으로 지구상의 모든 문자를 표현할 수 있는 문자 코드 체계입니다. 유니코드의 등장 배경과 내부적인 구성 원리 등의 자세한 사항에 대해서는 따로 설명하겠습니다. 유니코드를 지원하려면 문자형이나 문자열에 대해 C언어의 타입을 바로 쓰지 말고 유니코드 설정에 따라 변경되는 중간 타입을 사용해야 합니다. C언어에 익숙한 사람들은 앞으로 문자나 문자열을 표현할 때 다음 표현들을 써야합니다.

 C타입

 유니코드타입

 char

 TCHAR

 char *

 LPSTR

 const char*

 LPCTSTR


TCHAR은 C의 기본 타입 중 하나인 char와 일단 같지만 유니코드로 컴파일할 때는 wchar_t타입이 됩니다. wchar_t는 실제로는 unsigned short로 정의되어 있으며 부호없는 16비트 정수형입니다. TCHAR타입의 실제 정의문은 다음과 같이 조건부 컴파일문으로 작성되어 있습니다.

1
2
3
4
5
#ifdef UNICODE
typedef wchar_t TCHAR;
#else
type char TCHAR;
#endif
cs

char를 바로 쓴 소스는 유니코드로 바꿀 때 일일이 소스를 뜯어 고친 후 다시 컴파일해야 하지만 TCHAR라는 중간 타입을 사용한 소스는 프로젝트 설정에 따라 소스도 같이 바뀌는 효과가 있으므로 소스는 그대로 두고 컴파일만 다시 하면됩니다. 문자열이 필요할 때도 char *를 쓰지 말고 가급적이면 LPSTR 또는 TCHAR*를 쓰는 것이 현명합니다. 문자열을 다루는 함수들도 C의 표준함수를 쓰지 말고 가급적이면 유니코드를 인식하는 함수를 사용합니다. 유니코드 인식 함수들은 표준 함수와 이름이 비슷하되 앞에 l자가 앞에 하나 더 붙는다는 것만 다르며 운영체제가 제공하는 API함수이므로 별도의 용량을 차지하지 않는다는 장점이 있습니다.


 C 표준 함수

 유니코드 지원 함수

 strlen

 lstrlen

 strcpy

 lstrcpy

 strcat

 lstrcat

 strcmp

 lstrcmp

 sprintf

 lsprintf


strlen은 char 타입의 문자열 길이만 조사하지만 lstrLen은 TCHAR 타입의 문자열에 대해서도 동작하므로 이식에 훨씬 더 유리합니다. 무자열 상수도 타입이 있으므로 겹 따옴표표안에 바로 문자열 상수를 쓰지말고 TEXT 매크로로 둘러 싸는것이 좋습니다.

1
2
TCHAR *str="string";
TCHAR *str=TEXT("string");
cs

TEXT매크로는 유니코드 설정에 따라 문자열 상수의 타입을 변경합니다. 유니코드로 컴파일할 때는 각 문자가 16비트의 유니코드 문자가 되며 그렇지 않을 떄는 8비트의 안시(ANSI)문자가 됩니다. 모든 문자열 상수에 일일이 TEXT 매크로를 쓰는 것은 실습에는 방해가 되므로 실습중에는 이 매크로를 종종 생략하기도 하는데 원칙적으로 문자열 상수에 이 매크로를 쓰는 것이 좋습니다.


유니코드로 컴파일 하면 세계의 모든 문자를 단일한 코드 체계로 표현할 수 있어 국제화에 유리하다는 장점이있습니다. 하지만 윈도우즈 95/98이 유니코드를 지원하지 않으므로 유니코드로 컴파일된 프로그램은 95/98에서는 실행되지 않습니다. 하지만 현재는 사용하지 않으므로 유니코드로 많이 합니다.


4. 64비트 API

 
32비트 환경에 비한 64비트의 장점은 풍부한 메모리 공간으로 둘 수 있습니다. 32비트에서는 4G까지의 메모리를 지원했으나 64비트는 16TB라는 훨씬 더 넓은 메모리를 쓸 수 있습니다. 일반 사용자들엣게는 4G정도면 충분하지만 서버급에서는 4G라는 메모리의 한계가 들어납니다. 메모리 DB나 동영상 편집, 3D 그래픽 작업에는 4G메모리는 부족합니다. 메모리 공간이 넓어지면 훨씬 더 적은 수의 서버로도 더 많은 사용자의 요구를 처리할 수 있어 엄청난 비용을 절감할 수 있으며 과거에는 시도하기 힘들었던 대용량의 처리도 가능해집니다.


언젠가는 모든 사용자들이 64비트의 혜택을 받고 64비트 응용프로그램을 사용하겠지만 아직까지는 64비트를 기준으로 개발하기는 시기상조라고 할 수 있습니다. 64비트 환경이 대중화되기 위해서는 운영체제뿐만 아니라 개발툴과 일반 응용 프로그램까지 모두 64비트 환경이 대중화 되기 이해서는 운영체제 뿐만아니라 개발툴과 일반 응용 프로그램까지 모두 64비트로 이전해야 하는데 이는 예상보다 훨씬 더 많은 시간이 걸립니다. 사용자들은 32비트 시스템으로도 부족함을 느끼지 않으며 64비트 시스템을 구입할 생각이 없습니다. 일반 사용자에게 16TB의 메모리 용량이나 64개의 CPU지원은 당최 실감나지 않는 기능입니다. 앞으로도 사용자들은 상단기간동안 32비트 환경에서 작업을 할 것이며 따라서 응용 프로그램도 32비트 개발이 주를 이룰 것입니다.


그러나 아무리 미래의 일이라 하더라도 최소한의 대비는 해야합니다. 16비트 환경에서 32비트의 이전은 가히 충격적이라고 할만큼 많은 것이 바뀌었지만 다행히 32비트에서 64비트로의 이전은 큰 변화가 거의 없습니다. Win32의 모든 함수들은 거의 변화없이 64비트에서도 지원되며 32비트 응용 프로그램들도 약간의 노력으로 쉽게 64비트로 포팅할 수 있습니다. 현재 단계에서 가장 신경써야 할 일은 하나의 소스로 32비트와 64비트 실행 파일을 동시에 컴파일 할 수 있도록 이식성을 확보하는 것입니다.


64비트 CPU종류에 따라 각각 컴파일해야 하므로 하나의 소스로 최소한 3개의 실행 파일을 만들어내야 합니다. 사실 여기에 전문가용, 일반용 등의 등급과 언어별 버전까지 합하면 무수히 많은 버전이 나올 수 있습니다. 각 실행 파일별로 소스를 따로 작성하지 않고 하나의 소스로 여러 개의 실행 파일을 만들어 내야 하는 가장 큰 이유는 컴파일은 기계가 하지만 소스는 사람이 관리해야 하기 떄문입니다. 소스를 이식성있게 잘 관리하면 스위치만 바꿔 어떤 종류의 실행파일이든지 소쉽게 릴리즈 할 수 있습니다.


64비트에서 달라지는 가장 큰 점은 주소공간이 확대됨으로 해서 포인터의 크기가 64비트로 확장되었다는 점입니다. 포인터 타입외에 int나 long은 여전히 32비트를 유지하는데 64비트로 확장하면 낭비가 너무 심하기 떄문입니다. 64비트 개발 환경은 이식의 편의를 위해 여러가지 중간 타입을 제공하는데 고정 타입보다는 가급적이면 중간 타입을 사용하는 것이 유리합니다. 다음 타입들은 크기가 고정되어 있는데 타입의 이름에 부호 여부와 길이가 잘 나타나있습니다.


 길이

 부호있음

 부호없음

 32비트

 INT32, LONG32

 UINT32, ULONG32, DWORD32

 64비트

 INT64, LONG64

 UINT64, ULONG64, DWORD64


실행되는 환경에 상관없이 일정한 길이를 가져야 한다면 이 타입을 사용합니다. INT32는 항상 32비트의 부호 있는 정수형이며 INT64는 항상 부호있는 64비트 정수입니다. 컴파일되는 환경에 따라 길이가 달라져야 한다면 다음 타입들을 사용합니다.


INT_PTR, LONG_PTR, UINT_PTR, ULONG_PTR, DWORD_PTR,


뒤에 _PTR이 붙은 타입은 운영체제의 비트수에 따라 크기가 가변적이며 포인터와 같은 길이를 가집니다. 포인터와 호환되어야 하는 정수가 필요할 떄는 이 타입을 사용해야 합니다. 예를 들어 어떤 인수가 부호없는 정수 또는 포인터 타입일 수 있다면 INT나 int대신 INT_PTR을 사용합니다. INT는 항상 32비트이지만 INT_PTR 타입은 32비트 환경에서는 32비트이며 64비트에서는 64비트입니다. 다음 예를 통해 이런 중간 타입이 어떤 연할을 하는지 확인해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void DisplayInfo(int Type, int Value)
{
    TCHAR info[128];
    switch(Type)
    {
    case 1:
        wsprintf(info,"나이는 %d입니다..",Value);
        break;
    case 2:
        wsprintf(info,"이름을 %s입니다..",(TCHAR *)Value);
        break;
    }
    MessageBox(hWnd,Info,"정보",MB_OK);
}
cs

이 함수는 인수로 전달된 어떤 값을 출력하는데 Type은 정보의 종류를 지정하며 Value는 실제 출력할 데이터입니다. 이 함수로 나이, 이름, 주소, 형액형 등 아주 다양한 타입의 정보가 전달된다면 데이터의 타입을 하나로 고정하고 Type에 따라 적절히 캐스팅해서 사용하는 것이 일반적입니다. 예에서는 int로 받되 문자열일 경우 TCHAR *로 캐스팅했는데 TCHAR *로 받아서 int로 캐스팅해도 마찬가지입니다. 이 함수를 호출할 때는 형식 인수의 타입에 맞게 실인수를 캐스팅해야 합니다.

1
2
DisplayInfo(1.25);
DisplayInfo(2,(int)"홍길동");
cs


무자열 상수를 (int)로 캐스팅해서 넘기면 받는 쪽에서 다시 (TCHAR *)로 캐스팅해서 사용하므로 논리상 문제가 없습니다. 이 코드가 잘 동작하는 이유는 정수형과 포인터형이 둘 다 32비트로 길이가 동일하기 떄문입니다. 그러나 64비트에서는 두 타입의 길이가 다르기 때문에 이런 코드가 제대로 동작하지 않습니다. 문자열 상수의 범위가 32비트를 넘을 수 있어 (int)로 캐스팅되는 과정에서 상위 32비트가 버려지므로 엉뚱한 번지를 참조하게 될 것입니다. 이럴 때 중간 타입을 사용하는데 함수 원형과 호출문을 다음과 같이 수정하면 됩니다.

1
2
void DisplayInfo(int Type, INT_PTR Value);
DisplayInfo(2,(INT_PTR)"홍길동");
cs


(INT_PTR은 포인터와 호환되는 정수형이므로 32비트, 64비트 모두 잘 컴파일 됩니다. INT_PTR은 아마도 다음과 같이 정의 되어 있을 것입니다.

1
2
3
4
5
#if defined(_WIN64)
    typedef __int64 INT_PTR;
#else
    typedef int INT_PTR;
#endif
cs


64비트 환경에서 64비트의 정수가 되며 32비트 환경에서는 32비트의 정수가 되므로 컴파일되는 환경에 맞는 길이를 가집니다. DWORD_PTR, LONG_PTR등의 중간 타입도 마찬가지 방식으로 정의되어 있습니다. 포인터와 정수 양쪽을 대입받을 수 있는 타입은 이식성 확보를 위해 반드시 이런 중간 타입을 사용해야 합니다. 물론 완벽한 이식성을 확보하려면 중간 타입을 쓰는 정도만으로 부족하고 훨씬 더 섬세한 부분까지 신경써야 하며 두 환경의 미세한 차이점에 대해서도 많은 것을 알아야 합니다.그러나 API학습을 시작하는 지금 단계에서 이런 것 까지 이해할 필요는 없으며 현실적으로 이해할 수도 없습니다. 그래서 당분간은 32비트 환경에서의 학습에만 열중하겠습니다. 32비트를 모르면 64비트를 할 수 없습니다.

'Programming > Windows API' 카테고리의 다른 글

WinApi - 출력(2/2)  (0) 2015.08.29
WinApi - 출력(1/2)  (0) 2015.08.27
WinApi - 분석(2/2)  (0) 2015.08.26
WinApi - 분석(1/2)  (1) 2015.08.26
WinAPI - 첫번째 시작 - 프로젝트 만들기  (0) 2015.08.25