Python에서 Win32 API 활용하기 - ctypes 모듈
System/Cybersecurity

Python에서 Win32 API 활용하기 - ctypes 모듈


개요


  Python으로 Windows OS에서 제공되는 강력한 기능을 활용하려면 Win32 API를 활용해야 한다.

이를 위해 Python용 외부 함수(foreign function) 라이브러리인 ctypes를 활용해 보자.

ctypes는 C 호환 데이터형을 제공하며, DLL 또는 공유 라이브러리에 있는 함수를 호출할 수 있다.

또한 Windows, Linux, Unix, OS X, Android 등 다양한 운영체제에서 지원하는 Native Library를 사용할 수 있는 강력한 도구이기도 하다.


  ctypes는 동적 라이브러리 호출 절차를 단순화하고, 복잡한 C 데이터 형을 지원하며 Low Level 함수를 제공한다는 장점이 있다.



DLL 로딩

  ctypes는 cdll, windll, oldell 호출 규약을 지원한다.


ctypes

지원하는 호출 규약

비고

cdll

cdecl


windll

stdcall


oledll

stdcall

반환 값을 HRESULT로 가정함


  DLL 로드를 위해 cdll을, Windows에서는 windll, oledll 객체를 사용한다. cdll은 표준 cdecl, windll은 stdcall 호출 규약을 사용하여 함수를 호출한다.

oledll 또한 windll처럼 stdcall 호출 규약을 사용하는데, 함수가 HRESULT 에러 코드를 return한다고 가정한다는 차이점이 있다.

에러 코드는 함수 호출이 실패할 때 OSError 예외를 자동으로 발생시키는 데 사용된다.


1
2
3
4
5
>>> from ctypes import *
>>> print windll.kernel32
<WinDLL 'kernel32', handle 771e0000 at 2f2e950>
>>> print cdll.msvcrt
<CDLL 'msvcrt', handle 76260000 at 2f22670>
cs


  Windows 용 예제. msvcrt[각주:1]는 대부분의 표준 C 함수가 포함된 Microsoft 표준 C 라이브러리이며, cdecl 호출 규약을 사용한다.

윈도우는 일반적으로 .dll 파일 접미사를 자동으로 추가한다.

Linux에서는 라이브러리를 로드하려면 확장자를 포함하는 파일명을 지정해야 하므로, 어트리뷰트 액세스로 라이브러리를 로드할 수 없고, DLL Loader의 LoadLibrary() 함수를 사용하거나 cdll의 생성자를 호출하여 로드해야 한다.


Win32 API 호출

  DLL 이름 뒤에 호출하고자 하는 함수명을 붙여준다. API를 호출할 때 전달하는 인자의 자료형을 지정할 수 있다.

1
2
3
4
5
6
>>> from ctypes import *
>>> libc = cdll.msvcrt
>>> printf = libc.printf
>>> printf.argtypes = [c_char_p, c_char_p, c_int, c_double]
>>> printf("String '%s', Int %d, Double %f\n""Hi"102.2)
String 'Hi', Int 10, Double 2.200000
cs

  msvcrt 라이브러리를 호출하여 라이브러리에 들어있는 C 함수 printf를 Python에서도 사용할 수 있게 하였다.
한편 함수의 반환 값 형식을 지정하려면 아래와 같이 선언하면 된다.

1
libc.strchr.restype = c_char_p
cs

  위 예시는 함수의 반환 값 형식을 c_char_p (문자, char) 형식으로 지정하였다.
함수는 dll 객체의 어트리뷰트로 접근된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> from ctypes import *
>>> libc = cdll.msvcrt
>>> libc.printf
<_FuncPtr object at 0x0361A828>
>>> print windll.kernel32.GetModuleHandleA
<_FuncPtr object at 0x0361A990>
>>> print windll.kernel32.MyOwnFunction
Traceback (most recent call last):
  File "<stdin>", line 1in <module>
  File "C:\python27\lib\ctypes\__init__.py", line 375in __getattr__
    func = self.__getitem__(name)
  File "C:\python27\lib\ctypes\__init__.py", line 380in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: function 'MyOwnFunction' not found
>>>
cs


  kernel32와 user32와 같은 Win32 시스템 DLL은 ANSI 뿐만 아니라 유니코드 버전 함수도 사용할 수 있다. 유니코드 버전은 이름에 W가 추가로 붙어있고, ANSI 버전은 이름에 A가 붙어 있다.


1
2
HMODULE GetModuleHandleA(LPCSTR lpModuleName);    # ANSI
HMODULE GetModuleHandleW(LPCWSTR lpModuleName);    # UNICODE
cs


  GetModuleHandle 함수는 인자로 받은 모듈 이름의 모듈 핸들[각주:2]을 반환한다. 위의 코드는 GetModuleHandle() 함수의 C 프로토타입이며, ANSI인지, 유니코드인지에 따라 GetModuleHandleA() 나 GetModuleHandleW를 명시적으로 지정하여 각각 필요한 버전에 따라 사용한다.


  유효한 Python 식별자가 아닌 이름으로 함수를 내보낼 때에는 getattr()[각주:3] 함수를 사용하여 함수를 조회해야 한다.


1
2
>>> getattr(cdll.msvcrt, "??2@YAPAXI@Z")
<_FuncPtr object at 0x0361AA08>
cs


  윈도우에서 일부 dll은 이름 대신 서수(ordinal)로 함수를 내보낸다. 아래 코드는 dll 객체를 인덱싱하여 액세스할 수 있도록 하는 간단한 함수를 구현한 것이다.


1
2
3
4
5
6
7
8
9
>>> cdll.kernel32[1]
<_FuncPtr object at 0x0361AA80>
>>> cdll.kernel32[0]
Traceback (most recent call last):
  File "<stdin>", line 1in <module>
  File "C:\python27\lib\ctypes\__init__.py", line 380in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: function ordinal 0 not found
>>>
cs



자료형

  Python에서는 자료형이라는 개념이 미약하다. 하지만 ctypes 모듈에서 제공하는 자료형을 이용하여 C 언어의 자료형을 사용할 수 있다.
C언어의 정수형을 사용하려면 다음과 같이 ctypes를 활용한다.

1
2
3
4
5
>>> from ctypes import *
>>> libc.strchr.restype = c_char_p
>>> i = c_int(42)
>>> print i.value
42
cs

  주소를 저장하는 포인터형을 선언해서 사용할 수도 있다.

1
PI = POINTER(c_int)
cs

  ctypes에서는 기본적인 C 호환 데이터형을 정의하고 있다. 아래 표는 그것들을 정리해놓은 것이다. 참고하도록 해 보자.

ctypes형

C형

Python 형

 c_bool

 _Bool

 bool(1)

 c_char

 char

 1 문자 바이트열 객체

 c_wchar

 wchar_t

 1 문자 문자열

 c_byte

 char







int

 c_ubyte

 unsigned char

 c_short

 short

 c_ushort

 unsigned short

 c_int

 int

 c_uint

 unsigned int

 c_long long
 c_ulong unsigned long
 c_longlong __int64 또는 long long
 c_ulonglong

 unsinged __int64 또는

 unsigned long long

 c_size_t size_t
 c_ssize_t ssize_t 또는 Py_ssize_t

 c_float

 float


float

 c_double double
 c_longdouble

 long double

 c_char_p char * (NULL 종료됨)

 바이트열 객체 또는 None

 c_wchar_p

 wchar_t * (NULL 종료함)

 문자열이나 None

 c_void_p

 void *

 int 또는 None


  생성자는 논리 값을 가진 모든 객체를 받아들인다.

  c_char_p, c_wchar_p 및 c_void_p 포인터형의 인스턴스에 새로운 값을 대입하면 포인터가 가리키는 메모리의 위치가 변경된다. 여기서 주의해야 할 것은 메모리 위치가 변경되는 것이지, 메모리 블록의 내용이 바뀌는 것이 아니다. Python에서 byte열 객체는 변하지 않기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> s = "Hello, World!"
>>> c_s = c_wchar_p(s)
>>> print c_s
c_wchar_p(139966785747106)
>>> print c_s.value
Hello World
>>> c_s.value = "Hi, there!"
>>> print c_s
c_wchar_p(139966783348656)
>>> print c_s.value
Hi, there!
>>> print s
Hello, World!
>>>
cs

  "Hello, World!"라는 값을 가지고 있던 변수 s에 "Hi, there!"라는 새로운 값을 대입하자 메모리의 위치가 변하는 것을 볼 수 있다. 하지만 변수 s의 메모리 블록의 내용은 여전히 "Hello, World!" 이다.

포인터의 전달

  bref() 함수는 매개 변수를 참조하는 데 사용된다. pointer() 함수로도 같은 결과를 얻을 수 있는데, byref()는 pointer()와는 다르게 실제 포인터 객체를 생성하지 않기 때문에 Python 자체에서 포인터 객체가 필요하지 않다면 byref()를 사용하는 것이 더 빠르다.


1
2
3
4
5
6
7
8
9
10
11
12
13
>>> from ctypes import *
>>> libc = cdll.msvcrt
>>>
>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer(b'\000' * 32)
>>> print i.value, f.value, repr(s.value)
0 0.0 ''
>>> libc.sscanf(b"1 3.14 Hello", b"%d %f %s", byref(i), byref(f), s)
3
>>> print i.value, f.value, repr(s.value)
1 3.1400001049 'Hello'
>>>
cs


콜백 함수[각주:7]

  먼저 콜백 함수를 위한 클래스를 만들어야 한다. 클래스는 호출 규약, 반환형 및 받는 인자의 수와 자료형을 명시한다.
CFUNCTYPE() 팩토리 함수는 cdecl 호출 규약을 사용하여 콜백 함수의 형태를 만들어내는 함수이다. Windows에서 사용되는 WINFUNCTYPE() 팩토리 함수는 stdcall 호출 규약을 사용한다.

1
2
3
4
>>> IntArray5 = c_int * 5
>>> ia = IntArray5(5173399)
>>> qsort = libc.qsort
>>> qsort.restype = None
cs

  위의 코드는 qsort() 함수를 사용하는 예제이다. qsort()는 콜백 함수의 도움을 받아 항목을 정렬하는 데 사용되는 표준 C 라이브러리의 함수로, 정렬할 데이터에 대한 포인터, 데이터 배열의 항목 수, 각 항목의 크기 및 비교 함수에 대한 포인터인 콜백으로 호출해야 한다. 콜백은 두 개의 포인터로 호출되는데, 첫 번째 인자가 두 번째보다 작으면 음의 정수를, 같으면 0을, 크면 양수 정수를 반환한다. 정리하자면 콜백 함수는 정수에 대한 포인터들을 받고 정수를 반환한다. 먼저 콜백 함수의 형태를 만든다.

1
>>> CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
cs

  아래 코드는 전달된 값을 보여주는 간단한 콜백이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from ctypes import *
 
libc = cdll.msvcrt
IntArray5 = c_int * 5
ia = IntArray5(5173399)
qsort = libc.qsort
qsort.restype = None
 
CMPFUNC = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
 
def py_cmp_func(a, b):
    print "py_cmp_func", a[0], b[0]
    return 0
 
cmp_func = CMPFUNC(py_cmp_func2)
qsort(ia, len(ia), sizeof(c_int), cmp_func)
cs



  이제 실제로 두 항목을 비교하여 보자. py_cmp_func() 함수를 다음과 같이 변경한 후 실행해 본다..

1
2
3
def py_cmp_func(a, b):
    print "py_cmp_func", a[0], b[0]
    return a[0- b[0]
cs





구조체


  ctypes 모듈에서의 구조체는 Structure 클래스를 상속받아 선언된다.

1
2
3
4
5
6
7
8
9
10
11
12
from ctypes import *
class POINT(Structure):
    _fields_ = [("x", c_int), ("y", c_int)]
 
point = POINT(1020)
print(point.x, point.y)
 
point = POINT(y=5)
print(point.x, point.y)
 
POINT(123)
print(point.x, point.y)
cs


  Structure를 상속하여 만들어진 클래스는 _fields_라는 값을 정의해야 하는데, 예제에서 보여준 것과 같이 _fields_ = [("필드 이름1", 필드 자료형1), ("필드 이름2", 필드 자료형2), ... ] 와 같은 형식의 리스트로 선언된다. 필드 자료형은 ctypes형이거나 다른 ctypes 자료형(구조체, 배열 포인터 등)이어야 한다. 위의 예제는 c_int 형의 두 개의 정수 x와 y를 가진 POINT 구조체를 구현한 것으로, 생성자에서 구조체를 초기화하는 방법도 볼 수 있다.

  1. ※ 주의 : cdll.msvct를 통해 표준 C 라이브러리에 액세스하면 Python에서 사용되는 라이브러리와 호환되지 않는 오래된 라이브러리 버전이 사용된다. 가능하면 Python 자체의 기능을 사용하거나 msvcrt 모듈을 import 해서 사용하는 것을 권장한다. [본문으로]
  2. 핸들(handle) : API에서 핸들은 32bit 크기의 숫자로 객체를 참조하는 것이다. Windows의 핸들은 C언어나 MS-DOS 프로그래밍의 파일 핸들과 유사하다. 프로그램은 거의 항상 Windows 함수를 호출함으로써 핸들을 얻는다. [본문으로]
  3. getattr(object, name[, default]) : 주어진 이름의 object 어트리뷰트를 반환한다. [본문으로]
  4. create_string_buffer(init_or_size, size = NONE) : 가변 문자 바퍼 생성. 반환된 객체는 ctypes c_char 배열이다. [본문으로]
  5. sscanf(const char *buffer, const char *format, argument-list); : buffer에서 argument-list가 제공하는 위치로 데이터를 읽는다. 각 argument는 format-string에서 유형 지정자에 대응하는 유형의 변수에 대한 포인터이다. [본문으로]
  6. repr(object) : 객체의 인쇠 가능한 표현을 포함한 문자열 반환 [본문으로]
  7. 콜백 함수(Callback function) : 다른 함수의 인자로써 이용되는 함수, 또는 어떤 이벤트에 의해 호출되어지는 함수 [본문으로]
  8. ※ 주의 : C 코드에서 사용되는 동안, CFUNCTYPE() 객체에 대한 참조를 유지해야 한다. ctypes가 계속 참조하고 있지는 않으며, 사용자가 직접 하지 않는다면 콜백이 발생할 때 프로그램이 충돌할 수도 있다. [본문으로]