본 글은 아래 환경에서 테스트한 코드로 작성했다.
- OS: Window 11
- Python: 3.13.0
- GCC(MinGW-W64): 13.3.0
- Go: 1.23.3
추가로 MacOS에서도 동일한 코드로 작동하는 것을 확인했다.
개요
- 언어(C, Go)를 공유 라이브러리로 빌드한다.
- 컴파일된 라이브러리를 ctypes로 불러온다.
- 사용할 함수의 파라미터 및 리턴 자료형을 정의한다.
- 함수를 호출한다.
ctypes
ctypes는 dll 또는 공유 라이브러리를 불러올 수 있는 Python 내장 라이브러리다. C와 같이 외부 언어로 작성한 코드를 Python에서 실행하도록 도와준다.
C to Py
C를 공유 라이브러리로
// lib.c
float add_float(float a, float b) {
return a + b;
}
실수를 더하는 간단한 C 코드다. C를 ctype에서 불러오기 위해 공유 라이브러리로 빌드한다.
$ gcc -shared 파일명.c -o 파일명.so
참고로 본 글에서는 .so를 사용하지만 window 환경이라면 .dll로 빌드해도 사용 가능하다.
Python에서 불러오기
import ctypes
lib = ctypes.cdll.LoadLibrary("clib.so")
LoadLibrary를 이용해 .so 파일을 불러온다. 그리고 사용할 함수의 타입을 정의한다.
"""lib.c
float add_float(float a, float b)
"""
# 파라미터 타입
lib.add_float.argtypes = [ctypes.c_float, ctypes.c_float]
# 리턴 타입
lib.add_float.restype = ctypes.c_float
argtypes로 입력받을 자료형을, restype으로 반환하는 자료형을 정의한다. C 자료형은 공식문서에서 확인할 수 있다. 타입을 정의하지 않아도 실행은 가능하지만 예상과 다른 값이 반환될 수 있다.
a = ctypes.c_float(3.5)
b = ctypes.c_float(4.2)
res = lib.add_float(a, b)
print(res) # 7.699999809265137
입력값도 C 타입으로 정의하고 실행해 보면 예상대로 잘 작동한다. 정리하면 다음과 같다.
import os
import ctypes
cpp_file = os.path.join(os.getcwd(), "clib.so")
lib = ctypes.cdll.LoadLibrary(cpp_file)
# Add float
lib.add_float.argtypes = [ctypes.c_float, ctypes.c_float]
lib.add_float.restype = ctypes.c_float
a = ctypes.c_float(3.5)
b = ctypes.c_float(4.2)
res = lib.add_float(a, b)
print(res) # 7.699999809265137
예시: 구조체(배열) 사용
조금 더 복잡한 예시로 C에서 동적 배열을 만들고 Python에서 호출해 보자. C는 필요한 동작을 모두 함수 형태로 내보내고, Python은 객체 형태로 묶어 사용했다.
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t size;
size_t capacity;
} Array;
Array* initArray(size_t capacity) {
Array *arr = (Array *)malloc(sizeof(Array));
arr->size = 0;
arr->capacity = capacity;
arr->data = (int *)malloc(arr->capacity * sizeof(int));
return arr;
}
int size(Array *arr) {
return arr->size;
}
void append(Array *arr, int item) {
if (arr->size == arr->capacity) {
// growth factor: 2
arr->capacity *= 2;
arr->data = (int *)realloc(arr->data, arr->capacity * sizeof(int));
}
arr->data[arr->size] = item;
arr->size++;
}
int getItem(Array *arr, size_t index) {
if (index < arr->size) {
return arr->data[index];
}
}
void freeArray(Array *arr) {
free(arr->data);
free(arr);
}
import os
import ctypes
class ArrayStructure(ctypes.Structure):
"""C 구조체 Array 타입 정의"""
_fields_ = [
("data", ctypes.POINTER(ctypes.c_int)),
("size", ctypes.c_size_t),
("capacity", ctypes.c_size_t),
]
class Array(ctypes.Structure):
"""C를 활용한 동적배열"""
lib = ctypes.cdll.LoadLibrary(os.path.join(os.getcwd(), "arrlib.so"))
lib.initArray.argtypes = [ctypes.c_size_t]
lib.initArray.restype = ctypes.POINTER(ArrayStructure)
lib.size.argtypes = [ctypes.POINTER(ArrayStructure)]
lib.size.restype = ctypes.c_size_t
lib.append.argtypes = [ctypes.POINTER(ArrayStructure), ctypes.c_int]
lib.append.restype = None
lib.getItem.argtypes = [ctypes.POINTER(ArrayStructure), ctypes.c_size_t]
lib.getItem.restype = ctypes.c_int
lib.freeArray.argtypes = [ctypes.POINTER(ArrayStructure)]
lib.freeArray.restype = None
def __init__(self, capacity: int):
self.arr = self.lib.initArray(capacity)
def __del__(self):
self.lib.freeArray(self.arr)
def __len__(self) -> int:
return self.lib.size(self.arr)
def __getitem__(self, idx: int) -> int:
if idx < len(self):
return self.lib.getItem(self.arr, idx)
raise IndexError("index out of bounds")
def append(self, item: int):
self.lib.append(self.arr, item)
Array 포인터를 정의하기 위해 ArrayStructure를 생성해줬다. 그리고 Array 클래스를 통해 필요한 내용을 모두 선언하고 Python처럼 사용하기 위해 메서드를 정의해 줬다.
arr = Array(10)
for i in range(10):
arr.append(i)
print(len(arr)) # 10
for i in range(10):
print(arr[i], end=" ")
# 0 1 2 3 4 5 6 7 8 9
del arr
C++ to Py
C++도 같은 맥락이지만 g++로 인한 name mangling이 발생하지 않도록 해야 한다.
// lib.cpp
extern "C" {
float add_float(float a, float b) {
return a + b;
}
}
$ g++ -shared lib.cpp -o clib.so
Go to Py
Go → Python은 C를 거쳐 Go → C → Python의 과정을 거친다. 따라서 위에서 설명한 C to Py와 같은 맥락으로 볼 수 있다.
package main
import "C"
//export AddInt
func AddInt(a, b int) int {
return a + b
}
//export AddFloat
func AddFloat(a, b float32) float32 {
return a + b
}
func main() {}
반드시 C를 import 해주어야 한다. 그렇지 않으면 ctype에서 함수를 찾지 못한다. 그리고 내보낼 함수 위에 //export를 작성한다.
$ go build -buildmode=c-shared -o 파일명.so 파일명.go
파일을 빌드하면 .so 파일과 .h 헤더 파일이 생성된다. 이제 ctype으로 불러오면 동일하게 작동한다.
import os
import ctypes
go_file = os.path.join(os.getcwd(), "golib.so")
lib = ctypes.cdll.LoadLibrary(go_file)
# Add Int
res = lib.AddInt(5, 9)
print(res)
# Add Float
a = ctypes.c_float(3.5)
b = ctypes.c_float(4.2)
lib.AddFloat.restype = ctypes.c_float
res = lib.AddFloat(a, b)
print(res)