ctypes: C와 Go를 Python에서

본 글은 아래 환경에서 테스트한 코드로 작성했다.

  • OS: Window 11
  • Python: 3.13.0
  • GCC(MinGW-W64): 13.3.0
  • Go: 1.23.3

추가로 MacOS에서도 동일한 코드로 작동하는 것을 확인했다.


개요

  1. 언어(C, Go)를 공유 라이브러리로 빌드한다.
  2. 컴파일된 라이브러리를 ctypes로 불러온다.
  3. 사용할 함수의 파라미터 및 리턴 자료형을 정의한다.
  4. 함수를 호출한다.

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)