ctypes — 파이썬용 외부 함수 라이브러리

ctypes — 파이썬용 외부 함수 라이브러리

ctypes는 Python을 위한 외부 함수 라이브러리(FFI)예요. C 호환 데이터 타입을 제공하고, DLL이나 공유 라이브러리에 있는 함수를 호출할 수 있게 해 주죠. 이 라이브러리를 순수 Python으로 감싸서(wrap) 쓸 수 있게 해 줍니다.

출처: Python 표준 라이브러리

본문

[!WARNING] 경고 ctypes는 네이티브 라이브러리와 프로세스 메모리에 대한 하위 수준 접근을 제공해서, Python의 안전 장치를 우회하고 임의의 네이티브 코드를 실행할 수 있게 해요. 잘못 사용하면 데이터와 객체를 손상시키거나, 민감한 정보를 노출하거나, 크래시를 일으키거나, 실행 중인 프로세스를 손상시킬 수 있습니다.

ctypes는 선택적(optional) 모듈이에요.

ctypes 튜토리얼

참고: 일부 코드 예제는 ctypesc_int 타입을 참조해요. sizeof(long) == sizeof(int)인 플랫폼에서는 c_intc_long의 별칭이에요. 그래서 c_int를 기대했는데 c_long이 출력돼도 헷갈리지 마세요. 두 타입은 사실 같은 타입입니다.

동적 링크 라이브러리 로드하기

ctypes는 동적 링크 라이브러리를 로드하는 cdll 객체와, Windows에서는 windll, oledll 객체를 내보내요. 이 객체들의 속성으로 접근해 라이브러리를 로드할 수 있죠. cdll은 표준 cdecl 호출 규약을 쓰는 함수를 내보내는 라이브러리를 로드하고, windllstdcall 호출 규약을 써요. oledllstdcall 규약을 쓰면서 함수가 Windows HRESULT 오류 코드를 반환한다고 가정해요. 함수 호출이 실패하면 그 오류 코드로 자동으로 OSError 예외를 발생시킵니다.

Windows 예제를 볼게요. msvcrt는 대부분의 표준 C 함수를 담고 있는 MS 표준 C 라이브러리이고, cdecl 호출 규약을 사용해요:

>>> from ctypes import *
>>> print(windll.kernel32)
<WinDLL 'kernel32', handle ... at ...>
>>> print(cdll.msvcrt)
<CDLL 'msvcrt', handle ... at ...>
>>> libc = cdll.msvcrt
>>>

Windows는 보통 .dll 접미사를 자동으로 붙여줘요. 다른 시스템에서는 파일명을 확장자까지 포함해서 지정해야 하므로 특성 접근으로는 라이브러리를 로드할 수 없어요. LoadLibrary() 메서드를 쓰거나 CDLL 생성자를 호출해 인스턴스를 만들어야 하죠. 예를 들어 Linux에서는:

>>> cdll.LoadLibrary("libc.so.6")
<CDLL 'libc.so.6', handle ... at ...>
>>> libc = CDLL("libc.so.6")
>>> libc
<CDLL 'libc.so.6', handle ... at ...>

macOS에서는 libc.dylib를 쓰면 되고요.

로드된 DLL에서 함수 접근하기

함수는 dll 객체의 속성으로 접근해요:

>>> libc.printf
<_FuncPtr object at 0x...>
>>> print(windll.kernel32.GetModuleHandleA)
<_FuncPtr object at 0x...>
>>> print(windll.kernel32.MyOwnFunction)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "ctypes.py", line 239, in __getattr__
    func = _StdcallFuncPtr(name, self)
AttributeError: function 'MyOwnFunction' not found

kernel32, user32 같은 win32 시스템 dll은 함수의 ANSI 버전과 UNICODE 버전을 모두 내보내는 경우가 많아요. UNICODE 버전은 이름에 W가, ANSI 버전은 A가 붙어요. 예를 들어 GetModuleHandle 함수의 C 원형은 이렇게 돼요:

/* ANSI version */
HMODULE GetModuleHandleA(LPCSTR lpModuleName);
/* UNICODE version */
HMODULE GetModuleHandleW(LPCWSTR lpModuleName);

windll은 마술처럼 자동으로 선택하지 않으니, GetModuleHandleAGetModuleHandleW를 명시적으로 지정해서 각각 bytes나 문자열 객체로 호출해야 해요.

가끔 dll은 유효한 Python 식별자가 아닌 이름("??2@YAPAXI@Z" 같은)으로 함수를 내보내기도 해요. 그럴 땐 getattr()로 가져와야 하고요. 몇몇 dll은 이름 대신 서수(ordinal)로 함수를 내보내는데, dll 객체를 정수로 인덱싱해 접근할 수 있습니다.

함수 호출하기

이 함수들은 다른 Python callable처럼 호출할 수 있어요. 매개변수가 없는 rand()를 예로 들게요:

>>> print(libc.rand())
1804289383

Windows에서 GetModuleHandleA()를 호출하면 win32 모듈 핸들을 반환해요(NULL 포인터로 호출하려면 단일 인자로 None을 넘기면 돼요):

>>> print(hex(windll.kernel32.GetModuleHandleA(None)))
0x1d000000

cdecl 호출 규약으로 stdcall 함수를 호출하면(또는 그 반대면) ValueError가 발생해요:

>>> cdll.kernel32.GetModuleHandleA(None)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Procedure probably called with not enough arguments (4 bytes missing)

None, 정수, bytes 객체, (유니코드) 문자열만이 이 함수 호출에 직접 매개변수로 쓸 수 있는 네이티브 Python 객체예요. None은 C NULL 포인터로, bytes/문자열은 데이터를 담은 메모리 블록을 가리키는 포인터(char * 또는 wchar_t *)로 전달되고, Python 정수는 플랫폼 기본 C int 타입으로 전달돼요.

기본 데이터 타입

ctypes는 다양한 원시 C 호환 데이터 타입을 정의해요. 주요 목록:

ctypes 타입 C 타입 Python 타입
c_bool _Bool bool
c_char char 1문자 bytes
c_wchar wchar_t 1문자 str
c_byte char int
c_ubyte unsigned char int
c_short short int
c_ushort unsigned short int
c_int int int
c_uint unsigned int int
c_long long int
c_ulong unsigned long int
c_longlong long long int
c_ulonglong unsigned long long int
c_size_t size_t int
c_float float float
c_double double float
c_longdouble long double float
c_char_p char * (NUL 종료) bytes 또는 None
c_wchar_p wchar_t * (NUL 종료) str 또는 None
c_void_p void * int 또는 None
py_object PyObject * object

IEC 60559 호환 복소수 산술을 지원하는 플랫폼에는 c_float_complex, c_double_complex, c_longdouble_complex도 있어요.

이 모든 타입은 올바른 타입과 값의 선택적 초기화 인자를 넣어 호출해 생성할 수 있어요:

>>> c_int()
c_long(0)
>>> c_wchar_p("Hello, World")
c_wchar_p(140018365411392)
>>> c_ushort(-3)
c_ushort(65533)

이 타입들은 가변(mutable)이라 값도 나중에 바꿀 수 있어요:

>>> i = c_int(42)
>>> print(i)
c_long(42)
>>> print(i.value)
42
>>> i.value = -99
>>> print(i.value)
-99

가변 메모리 블록이 필요하면 create_string_buffer() 함수를 써요. NUL 종료 문자열로 접근하려면 value 속성을, 그 외에는 raw 속성을 쓰면 됩니다.

가변 인자 함수 호출하기

여러 플랫폼에서 ctypes를 통한 가변 인자 함수 호출은 고정 개수 매개변수 호출과 동일해요. 다만 일부 플랫폼(특히 Apple 플랫폼의 ARM64)에서는 가변 함수의 호출 규약이 달라요. 그런 플랫폼에서는 고정(비가변) 인자에 대해 argtypes 속성을 지정해야 해요:

libc.printf.argtypes = [ctypes.c_char_p]

커스텀 데이터 타입으로 함수 호출하기

ctypes는 인스턴스가 함수 인자로 쓰일 수 있도록 인자 변환을 커스터마이즈할 수 있어요. ctypes_as_parameter_ 속성을 찾아 함수 인자로 사용해요. 이 속성은 정수, 문자열, bytes, ctypes 인스턴스, 또는 _as_parameter_ 속성을 가진 객체여야 합니다.

필수 인자 타입 지정 (proto타입)

DLL에서 내보낸 함수의 필수 인자 타입은 argtypes 속성을 설정해 지정할 수 있어요:

>>> printf.argtypes = [c_char_p, c_char_p, c_int, c_double]
>>> printf(b"String '%s', Int %d, Double %f\n", b"Hi", 10, 2.2)
String 'Hi', Int 10, Double 2.200000
37

형식을 지정하면 호환되지 않는 인자 타입으로부터 보호해 주고, 인자를 유효한 타입으로 변환하려고 시도해요.

반환 타입

기본적으로 함수는 C int 타입을 반환한다고 가정해요. 다른 반환 타입은 함수 객체의 restype 속성으로 지정할 수 있어요. 예를 들어 time()time_t time(time_t *) 원형이므로 restype을 지정해요:

>>> libc.time.restype = c_time_t
>>> libc.time.argtypes = (POINTER(c_time_t),)
>>> print(libc.time(None))
1150640792

메모리 블록이 아니라 값만 필요하다면 restype에 callable Python 객체를 지정해 오류 반환 값을 검사하고 자동으로 예외를 발생시킬 수도 있어요.

포인터 전달하기 (참조로 매개변수 전달)

ctypes는 참조로 매개변수를 전달하는 byref() 함수를 내보내요. 같은 효과를 pointer()로도 얻을 수 있지만, pointer()는 실제 포인터 객체를 만들기 때문에 작업이 더 많아요. Python에서 포인터 객체 자체가 필요 없다면 byref()가 더 빠릅니다.

>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer(b'\000' * 32)
>>> print(i.value, f.value, repr(s.value))
0 0.0 b''
>>> 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 b'Hello'

구조체와 공용체

구조체와 공용체는 ctypes 모듈에 정의된 StructureUnion 기본 클래스에서 파생돼야 해요. 각 하위 클래스는 _fields_ 속성을 정의해야 하고, _fields_는 필드 이름과 필드 타입을 담은 2-튜플들의 리스트예요. 필드 타입은 c_int 같은 ctypes 타입이거나 구조체·공용체·배열·포인터 등 파생된 ctypes 타입이어야 해요.

>>> from ctypes import *
>>> class POINT(Structure):
...     _fields_ = [("x", c_int),
...                 ("y", c_int)]
...
>>> point = POINT(10, 20)
>>> print(point.x, point.y)
10 20
>>> point = POINT(y=5)
>>> print(point.x, point.y)
0 5

구조체는 그 자체로 다른 구조체를 필드 타입으로 포함할 수 있어요. 기본적으로 Structure·Union 필드는 C 컴파일러가 하는 것과 같은 방식으로 배치돼요. _pack__align_ 클래스 속성으로 최대 정렬을 지정할 수도 있고, BigEndianStructure·LittleEndianStructure·BigEndianUnion·LittleEndianUnion 기본 클래스로 비-네이티브 바이트 순서 구조체를 만들 수 있어요(이들은 포인터 필드를 담을 수 없어요).

구조체와 공용체에는 비트 필드도 만들 수 있는데, _fields_ 튜플의 세 번째 항목으로 비트 폭을 지정해요:

>>> class Int(Structure):
...     _fields_ = [("first_16", c_int, 16),
...                 ("second_16", c_int, 16)]

[!WARNING] 경고 ctypes는 비트 필드가 있는 공용체나 구조체를 값으로 함수에 전달하는 것을 지원하지 않아요. 32비트 x86에서는 동작할 수 있지만 일반적으로 보장되지 않아요. 비트 필드가 있는 공용체/구조체는 항상 포인터로 함수에 전달해야 합니다.

배열

배열은 같은 타입 인스턴스들을 고정 개수 담는 시퀀스예요. 배열 타입을 만드는 권장 방법은 데이터 타입에 양의 정수를 곱하는 거예요:

TenPointsArrayType = POINT * 10
>>> from ctypes import *
>>> TenIntegers = c_int * 10
>>> ii = TenIntegers(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> for i in ii: print(i, end=" ")
...
1 2 3 4 5 6 7 8 9 10

포인터

포인터 인스턴스는 ctypes 타입에 pointer() 함수를 호출해 만들어요. 포인터 인스턴스는 가리키는 객체를 반환하는 contents 속성이 있어요. ctypes는 OOR(원래 객체 반환)이 없어서 속성을 가져올 때마다 새롭고 동등한 객체를 만들어요.

>>> i = c_int(42)
>>> pi = pointer(i)
>>> pi.contents
c_long(42)
>>> pi.contents is i
False

POINTER() 함수는 아무 ctypes 타입을 받아 새 타입을 반환해요. 인자 없이 포인터 타입을 호출하면 NULL 포인터를 만들고, NULL 포인터는 거짓(bool) 값을 가져요.

콜백 함수

ctypes는 Python callable로부터 C 호출 가능한 함수 포인터를 만들 수 있게 해요. CFUNCTYPE() 팩토리 함수는 cdecl 호출 규약을 쓰는 콜백 타입을 만들고, Windows의 WINFUNCTYPE()stdcall 규약을 써요. 둘 다 결과 타입을 첫 인자로, 예상 인자 타입을 나머지 인자로 받아요.

표준 C 라이브러리의 qsort()를 정수 배열 정렬에 쓰는 예제를 볼게요:

>>> IntArray5 = c_int * 5
>>> ia = IntArray5(5, 1, 7, 33, 99)
>>> qsort = libc.qsort
>>> qsort.restype = None

콜백은 두 항목 포인터를 인자로 받아, 첫 항목이 더 작으면 음의 정수, 같으면 0, 더 크면 양의 정수를 반환해야 해요:

>>> 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 a[0] - b[0]
...
>>> qsort(ia, len(ia), sizeof(c_int), CMPFUNC(py_cmp_func))
py_cmp_func 5 1
...

팩토리 함수는 데코레이터 팩토리로도 쓸 수 있어요.

[!NOTE] 참고 C 코드에서 사용되는 동안 CFUNCTYPE() 객체에 대한 참조를 계속 유지하세요. ctypes는 유지하지 않으므로, 놓치면 가비지 컬렉션되어 콜백이 일어날 때 프로그램이 크래시할 수 있어요.

ctypes 참조

공유 라이브러리 찾기

find_library() 함수는 컴파일러나 런타임 로더가 하는 것과 비슷한 방식으로 라이브러리를 찾아 경로명을 반환해요. namelib 같은 접두사, .so·.dylib 같은 접미사, 버전 번호가 없는 라이브러리 이름이에요. 찾을 수 없으면 None을 반환합니다. 정확한 동작은 시스템에 따라 달라요. Linux에서는 /sbin/ldconfig, gcc, objdump, ld 같은 외부 프로그램을 실행해 찾고, macOS/Android에서는 시스템 표준 명명 규칙과 경로를 사용하고, Windows에서는 find_library("c") 같은 호출이 실패해 None을 반환해요.

>>> from ctypes.util import find_library
>>> find_library("m")
'libm.so.6'
>>> find_library("c")
'libc.so.6'

또한 ctypes.util.dllist() 함수는 현재 프로세스에 이미 로드된 공유 라이브러리 목록을 제공해요.

공유 라이브러리 로드하기

CDLL 인스턴스는 로드된 공유 라이브러리를 나타내요. 이 라이브러리의 함수는 표준 C 호출 규약을 쓰고 int를 반환한다고 가정해요. 호출 전에 Python GIL은 해제되고 호출 후에 다시 획득돼요. 다른 동작은 OleDLL, WinDLL, PyDLL 하위 클래스를 사용해요.

class ctypes.CDLL(name, mode=DEFAULT_MODE, handle=None, use_errno=False, use_last_error=False, winmode=None)

name은 열 공유 라이브러리의 경로명이에요. handleNone이면 플랫폼의 dlopen(3) 또는 LoadLibrary()로 로드해요. 이미 로드된 라이브러리에 대한 기존 핸들이 있으면 handle 인자로 넘겨 새 CDLL 객체로 감쌀 수도 있어요.

use_errno=True로 설정하면 시스템 errno 오류 번호에 안전하게 접근하는 메커니즘이 활성화되고, ctypes.get_errno()ctypes.set_errno()로 ctypes 전용 스레드-로컬 사본에 접근/변경해요. use_last_errorctypes.get_last_error()/set_last_error()는 Windows 오류 코드에 같은 메커니즘을 적용해요. winmode 매개변수는 Windows에서 Win32 LoadLibraryEx 플래그를 받아요.

관련 상수: RTLD_GLOBAL, RTLD_LOCAL, DEFAULT_MODE. 미리 만들어진 로더 객체로는 cdll(CDLL 인스턴스 생성), windll, oledll, pydll이 있고, Python C API에 직접 접근하는 pythonapi(PyDLL 인스턴스)도 있어요. LibraryLoader 클래스는 라이브러리를 로드하는 클래스예요.

유틸리티 함수

  • addressof(obj) — ctypes 인스턴스 메모리 버퍼의 주소를 정수로 반환
  • alignment(obj_or_type) — ctypes 타입의 정렬 요구사항 반환
  • byref(obj[, offset]) — 참조로 전달할 경량 포인터 반환
  • cast(obj, type) — C의 캐스트 연산자처럼, 같은 메모리 블록을 가리키는 새 타입 인스턴스 반환
  • create_string_buffer(init, size=None) — 가변 문자 버퍼 생성(c_char 배열)
  • create_unicode_buffer(init, size=None) — 가변 유니코드 버퍼 생성(c_wchar 배열)
  • memmove(dst, src, count), memset(dst, c, count) — C 라이브러리 함수와 동일
  • POINTER(type, /) — ctypes 포인터 타입 생성/반환
  • pointer(obj, /)obj를 가리키는 새 포인터 인스턴스 생성
  • resize(obj, size) — ctypes 객체 내부 메모리 버퍼 크기 조정
  • sizeof(obj_or_type) — ctypes 타입/인스턴스의 메모리 버퍼 크기(바이트) 반환
  • string_at(ptr, size=-1), wstring_at(ptr, size=-1) — 포인터 주소의 바이트/와이드 문자열 반환
  • memoryview_at(ptr, size, readonly=False) — 주소의 메모리를 참조하는 memoryview 반환 (3.14+)

데이터 타입

모든 ctypes 데이터 타입의 공통 기본 클래스는 비공개 _CData이고, 모든 기본 데이터 타입의 기본 클래스는 _SimpleCData예요. 기본 데이터 타입(c_char_p, c_int 등)은 외부 함수 호출 결과로 반환되거나 구조체 필드·배열 항목을 검색할 때 투명하게 네이티브 Python 타입으로 변환돼요.

구조화 데이터 타입 기본 클래스로는 Union, BigEndianUnion, LittleEndianUnion, BigEndianStructure, LittleEndianStructure, Structure가 있고, 배열·포인터 타입으로는 Array, _Pointer가 있어요.

예외

  • ctypes.ArgumentError — 외부 함수 호출이 전달된 인자 중 하나를 변환하지 못할 때 발생
  • ctypes.COMError(hresult, text, details) — COM 메서드 호출이 실패했을 때 발생 (Windows)

더 알아보기

  • ctypes.wintypes 모듈: HWND, WPARAM, DWORD 같은 Windows 전용 데이터 타입
  • msvcrt 모듈과 faulthandler 모듈: 크래시 디버깅에 유용