메뉴 건너뛰기


Developer > Application
2장. 멀티 플랫폼 C개발자 (플랫폼간 의존성 없애기)
 
이번장에서는 플랫폼 간의 같은 기능 혹은 유사 기능의 함수의 원형이 서로 다른 것들을 같게 맞춰주는 방법에 대해서 소개합니다.
이 부분은 제가 경험으로 알고 있는 것들을 정리하는 것이니깐... 모든 것을 다 정리했다고 할 수는 없습니다.
여러분들이 알고 있는 것들이 있다면 댓글이나 강좌로 소개해 주세요.
이런 이유로 이글은 아마도 계속해서 변경 혹은 추가 될 것 같네요.
한가지 더 이야기 하자면 이번 글은 유닉스/리눅스와 윈도우즈간에 맞게 개발된 소스를 최대한 수정없이 포팅하기 위한 노력입니다.
 
아무튼 시작해 보도록 하죠.
 
우리가 너무나 잘 사용하는 C함수들 중에 ANSI표준이 아닌 것들이 몇가지 있습니다. 뭐 제 생각에는 표준에 들어가야 좋을 것 같은데 제정시에 빠진 것들이겠죠. 뭐...
그래서 컴파일러 마다 같은 기능을 하는 것들인데도 표준이 아니라는 이유로 원형이나 사용법이 틀린 것들이 있습니다.
또 표준 함수더라도 OS의 차이에 의해 달라진 함수들이 있습니다.
그것들을 정리해 보도록 하겠습니다.
 
1. 기본 함수들
 
1) strcasecmp(), strncasecmp()  vs.  stricmp(), strnicmp()
 
이들 함수는 대소문자를 구별하지 않고 문자열이 같은지를 비교하는 함수이고 사용법도 같지만 함수명이 다릅니다. 그래서 다음과 같이 정의해서 사용하도록 하지요.
 
#if defined(WIN32) || defined(WIN64) #define strcasecmp(x,y) stricmp((x),(y)) #define strncasecmp(x,y,z) strnicmp((x),(y),(z)) #else #define stricmp(x,y) strcasecmp((x),(y)) #define strnicmp(x,y,z) strncasecmp((x),(y),(z)) #endif
 
 
2) mkdir()
 
유닉스계열에서는 파일 혹은 디렉토리를 생성할때 사용권한을 지정하여야 합니다.  일반적으로 권한은 0777과 같이 8진수로 표시하여 생성합니다. 실제로 생성은 이값과 쉘에 지정된 umask의 역 값을 AND연산시키서 생성되게 됩니다. 즉 권한의 값을 mode라고 한다면 (mode & ~umask)와 같이 만들어 집니다.
뭐 그건 그렇고... 프로그램에서 가장 많이 생성하는 파일의 권한은  0x666일 겁니다. 실행 파일을 프로그램안에서 만들지는 않을 거고... 0x666으로 읽고 쓰기 권한을 모두 풀어 줘도 어차피 umask에 의해 다시 지정될 것이니까... 그렇게 보안에 문제가 되지는 않을 겁니다.
그래서 다음과 같이 정의해서 사용할 수 있습니다.
 
#if defined(WIN32) || defined(WIN64) #define nickmkdir(x) mkdir((x)) #define nickmkdir2(x,y) mkdir((x)) #else #define nickmkdir(x) mkdir((x),(0666)) #define nickmkdir2(x,y) mkdir((x),(y)) #endif
 
이렇게 정의하고 nickmkdir()함수만 사용하면 되겠네요.
 
3) setenv() vs. _putenv()
 
환경변수의 값을 얻어오는 getenv()는 표준 함수로 존재하지만 환경변수를 설정하는 함수 putenv()외에 setenv()라는게 존재합니다. 그래서 다음과 같이 정의해서 사용합니다.
#if defined(WIN32) || defined(WIN64) int setenv(const char *var, const char *val, int replacep) { char *buf; if (!replacep) { char *old = getenv(var); if (old!=NULL) return -1; } buf = (char *)malloc(strlen(var) + strlen(val) + 2); if (buf==NULL) return -1; sprintf(buf,"%s=%s",var,val); _putenv(buf); return 0; } #define putenv(e) _putenv((e)) #else #define _putenv(e) putenv((e)) #endif
위와 같이 함수로 정의된 부분을 헤더로 포함시킬때는 문제가 있겠죠? 알아서 사용하세요.
 
4) 무효화 시켜도 무방한 함수들 
유닉스/리눅스와 윈도우즈간의 차이로 인해 프로그램 동작에는 꼭 필요하지 않지만 보안이나 기타 등등의 문제로 제공되는 함수가 있습니다.
단지 포팅을 위해서 그 기능을 없애는데 사용하려고 함수 자체를 다음과 같이 무효화 시킬 수도 있습니다.
 
#if defined(WIN32) || defined(WIN64) typedef int ssize_t; typedef int uid_t; typedef int gid_t; typedef int pid_t; #define setgid(x) 0 #define setuid(x) 0 #define setsid() 0 #define getuid(x) 0 #else #define O_BINARY 0 #endif
 
완전한 포팅이 되려면 결국은 다시 구현해야 겠죠? 
getuid()에서 0을 리턴한다는 것은 root의 권한이 돌려진다는 것을 명심하세요.
 
 
5) 기타
 
위와 같이 조금만 바꾸면 유닉스/리눅스와 윈도우즈 간에 같이 사용할 수 있는 것들이 꽤 있습니다.
다음과 같은 것들이 되겠습니다.
 
#if defined(WIN32) || defined(WIN64) #define snprintf _snprintf #define pipe(h) _pipe(h,4096,O_BINARY) #define popen _popen #define pclose _pclose #define stat _stat #else #define _snprintf snprintf #define _pipe(h,s,m) pipe(h) #define _popen popen #define _pclose pclose #define _stat stat #endif
 
물론 항상 올바른 동작을 한다는 것을 보장한다는 것은 아닙니다.
조심해서 사용하세요. 
 
 
 
2. 시간 관련 관련함수
 
1) sleep(), usleep(), nanosleep()  vs.  Sleep()
 
이들 함수는 모두 프로세스를 일정 시간 대기시키는 함수입니다. 함수명도 다르지만 함수의 사용법 또한 좀 다릅니다.
각각의 함수 원형은 다음과 같이 생겼습니다.
 
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
void usleep(unsigned long usec);
 
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
 
#include <winbase.h>
VOID Sleep(DWORD dwMilliseconds);
 
VC++에서 제공하는 Sleep()함수는 밀리초를 인자로 받아들이는 반면 GNU C계열에서 사용할 수 있는 sleep(), usleep(), nanosleep()함수는 인자로 각각 초단위, 마이크로초, 나노초의 시간을 받아 들입니다.
특히 nanosleep()함수의 경우는 timespec이라는 구조체인 req에 대기시간을 지정하여 넘겨야 하며 대기를 다하지 못한 시간 즉 남은 시간을 또 rem으로 받아야 합니다.
고로 다음과 같이 정의하여 사용하도록 합니다.
 
#if defined(WIN32) || defined(WIN64) #define sleep(x) Sleep((x)*1000) #else #include &lt;time.h&gt; #define Sleep(x) \ do{ \ struct timespec interval, remainder; \ interval.tv_sec = (unsigned int)((x)/1000); \ interval.tv_nsec = (((x)-(interval.tv_sec*1000))*1000000); \ nanosleep(&amp;interval, &amp;remainder); \ }while(0) #endif
 
 위의 정의로는 마이크로 초와 나노초 단위의 대기는 사용할 수 없지만 (VC++에서 밀리초 이상의 정밀도를 갖는 대기 시간의 함수를 제공하지 않아서...) 초단위 시간의 대기와 밀리초 단위 시간 대기의 함수는 sleep()과 Sleep()함수로 사용할 수 있을 것입니다.
 
위에서 VC++이 아닌 경우에 Sleep()함수를 정의하면서 usleep()을 사용하지 않고 nanosleep()을 사용한 이유는 제가 usleep()의 인수에 1000을 곱해서 Sleep()함수 처럼 사용하다가 보니깐 usleep()의 인수 usec의 데이터형인 unsigned long에 지정할 수 있는 값이 생각보다 작더군요. 생각해 보면 usleep()에서 1초를 대기 시키기 위해서 1000000의 값을 입력해야 하는데 이러다 보면 unsigned long의 일반적인 한계인 4294967296 값으로 지정할 수 있는 최대 지연 시간은 4294.967296초, 약 72분 정도 밖에 안됩니다. 그래서 nanosleep()함수를 사용하였습니다. 이런 사정을 아셨다면 필요에 따라 usleep()으로 바꾸고 1000을 곱해서 사용하셔도 되겠습니다.
 
3. Socket관련 함수
 
 
1) 소켓 관련 메크로와 타입
 
유닉스 계열에서는 socket()의 리턴값이 int형 인데 비해 윈도우즈에서는 SOCKET형의 값을 리턴합니다.
헤더를 뒤져보면 SOCKET형은 unsigned int형으로 되어 있는 것을 알 수 있습니다.
그렇다 보니 윈도우즈에서도 int형에 담으면 socket()함수의 리턴값을 받아도 상관은 없습니다.
또 다른 측면으로 보면 유닉스 계열의 socket()함수도 int형을 리턴하지만 결국 에러인 경우 만 -1을 리턴하고 나머지는 모두 양수를 리턴합니다.
즉 캐스팅을 해서 사용할 수 있다는 말이지요.
 
#if !defined(WIN32) &amp;&amp; !defined(WIN64) #define INVALID_SOCKET (~0) #define SOCKET_ERROR (-1) #define INADDR_NONE (-1) #define SOCKET int #endif
 
위와 같이 정의하고 소켓형은 int만 사용하도록 하면 되겠네요.
 
SOCKET형 말고도 소켓함수에서 사용되는 각종 메크로도 서로 호환되게 정의하였습니다.
 
 
2) socket초기화
 
윈도우즈의 winsock을 사용하기 위해서는 최초 사용시 socket의 초기화 과정이 필요합니다. 다 사용한 다음에는 winsock을 해제해야 하죠. 그런데 유닉스나 리눅스에는 그런게 필요없습니다. 그래서 이번에는 이것이 호환되도록 해 보겠습니다.
 
실제 코드를 보여드리기 전에 atexit()라는 VC에서만 제공하는 함수 하나를 소개하겠습니다.
함수의 원형은 다음과 같이 생겼는데요.
int atexit(atexit_t func);
프로그램이 실행 후 최종적으로 종료 되기 전에 어떤 특정 함수 하나를 실행 한 후 종료할 수 있도록 최종 실행 함수를 등록하는 역할을 하는 함수입니다.
여기서 인자인 func는 void func(void) 형태의 함수여야 합니다.
atexit()함수에 의해 여러개의 종료 함수가 등록될 수 있으며 종료시에는 등록된 순서의 역순으로 실행 됩니다. 정상적으로 등록되면 0을 에러시에는 0이외의 값을 리턴합니다.
이번에는 이 함수를 한번 사용해 보도록 하겠습니다.
 
 
윈도우즈를 위해서 winsock_init()이라는 함수와 winsock_clean()라는 함수를 만들어서 사용하는 것으로 하지요.
 
 
#if defined(WIN32) || defined(WIN64) char is_winsock_load=0; void winsock_clean(void) { if(is_winsock_load==1){ WSACleanup (); is_winsock_load=0; } } void winsock_init(void) { WORD ver; WSADATA wsadata; if(is_winsock_load==0){ ver = MAKEWORD(2,2); WSAStartup(ver,&amp;wsadata); is_winsock_load=1; atexit(winsock_clean); } } #else #define winsock_clean() (0) #define winsock_init() (0) #endif
 
소켓함수를 사용하려면 위의 winsock_init()을 한번 호출해 주면 되구요. 다 사용한 후에는 winsock_clean()을 한번 호출해 주면됩니다. winsock_clean()함수를 호출하는 것을 깜박했더라도 한번 호출했으면 자동으로 호출되니깐....
 
또한 대부분의 소켓 프로그램에서는 socket()함수를 사용하면서 시작하게 됩니다.
(아닌 경우도 많이 있긴 하지만...) 이렇게 socket()함수로 시작하는 프로그램의 작성시에는 위와 같이 winsock_init()과 winsock_clean()함수를 사용하지 않고도 자동으로 동작되도록 할 수 있습니다.
 
위의 코드를 조금 수정해 보겠습니다.
#if defined(WIN32) || defined(WIN64) char is_winsock_load=0; void winsock_clean(void) { if(is_winsock_load==1){ WSACleanup (); is_winsock_load=0; } } int winsock_init(void) { WORD ver; WSADATA wsadata; if(is_winsock_load==0){ ver = MAKEWORD(2,2); WSAStartup(ver,&amp;wsadata); is_winsock_load=1; atexit(winsock_clean); } return 1; } #define nicksocket(x,y,z) (winsock_init()==1)?socket((x),(y),(z)):-1 #else #define winsock_clean() (0) #define winsock_init() (0) #endif
 
이렇게 정의한 다음 nicksocket()함수만을 사용하면 되겠네요.
 
3) write(), read() vs. send(), recv() 
TCP상에서 데이터를 전송하고 받을 때 유닉스에서는 write()함수와 read()함수를 사용합니다. 이는 유닉스는 소켓역시 파일로 취급하기 때문이죠. 윈도우즈에서는 send()와 recv()를 사용하는데 아큐먼트 갯수도 다릅니다. 하지만 윈도우즈에서 특별한 경우가 아니면 마지막 아큐먼트를 0으로 사용하기 때문에 다음과 같은 정의가 가능하겠네요.
#if defined(WIN32) || defined(WIN64) #define write(a,b,c) send((a),(b),(c),0) #define read(a,b,c) recv((a),(b),(c),0) #else #define send(a,b,c,d) write((a),(b),(c)) #define recv(a,b,c,d) read((a),(b),(c)) #endif
  
 
4) close() vs. closesocket() 
 
생성된 소켓을 닫는 명령은 윈도우즈와 유닉스가 서로 다릅니다. 그런데 유닉스 계열은 소켓도 파일과 같이 취급되므로 close()함수로 닫을 수 있지만...
윈도우즈는 close()로 닫을 경우 문제가 발생할 소지가 있습니다.
그래서 항상 코딩시에 cloasesocket()만을 사용하기로 하고 다음과 같이 정의해서 사용합니다. 
 
#if !defined(WIN32) &amp;&amp; !defined(WIN64) #define closesocket(s) close((s)); #endif
 
 
 
4. 기타
 
몇가지 경우가 없어서 기타로 취급하는 함수들을 이 곳에 적었습니다.
 
1) dlopen(), dlsym(), dlclose() vs. LoadLibrary(), GetProcAddress(), FreeLibrary()
 
이번에는 *.dll이나 *.so 혹은 *.sl파일을 오픈하고 해당 함수의 주소는 알아오는 함수에 대한 이야기 입니다.
 
#if defined(WIN32) || defined(WIN64) #define dlopen(path,flag) LoadLibrary((path)); #define dlsym(handle,module) GetProcAddress((handle),(module)); #define dlclose(handle) FreeLibrary((HINSTANCE)(handle)); #else #define LoadLibrary(path) dlopen((path), RTLD_LAZY); #define GetProcAddress(handle,module) dlsym((handle),(module)); #define FreeLibrary(handle) dlclose((handle)); #endif
 
위와 같이 정의하고 사용하면 되는데 사실 이게 정확히 같다고 말 할 수는 없습니다. 에러 체크를 해야 한다면 에러를 체크하는 방법이 서로 다르게 되니깐 말이죠.
이부분은 여러분들의 숙제로 남겨 놓겠습니다.
 
 
뭐 정리하면 많을 줄 알았는데... 그리 많지 않네요.
더 생각이 나면 그때 그때 정리하도록 하겠습니다.
그럼 이번 강좌는 여기까지...
 
 
Creative Commons License
Creative Commons License이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-동일조건변경허락 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
Copyright 조희창(Nicholas Jo). Some rights reserved. http://bbs.nicklib.com