1. 程式人生 > >使用ctypes在Python中調用C++動態庫

使用ctypes在Python中調用C++動態庫

sage contex count free ini val 結果 struct 加法

使用ctypes在Python中調用C++動態庫

入門操作

使用ctypes庫可以直接調用C語言編寫的動態庫,而如果是調用C++編寫的動態庫,需要使用extern關鍵字對動態庫的函數進行聲明:

#include <iostream>
using namespace std;

extern "C" {
    void greet() {
        cout << "hello python" << endl;
    }
}

將上述的C++程序編譯成動態鏈接庫:

g++ hello.cpp -fPIC -shared -o hello.so

在Python代碼中使用ctypes導入動態庫,調用函數:

# -*- coding: utf-8 -*- #
from ctypes import CDLL

hello = CDLL('./hello.so')

if __name__ == '__main__':
    hello.greet()

運行上述Python程序:

xujijun@pc:~/codespace/python$ python3 hello.py
hello python

參數傳遞

編寫一個整數加法函數

#include <iostream>
using namespace std;

extern "C" {
    int add(int a, int b) {
        return a + b;
    }
}

編譯得到動態庫,在Python代碼中調用:

# -*- coding: utf-8 -*- #
from ctypes import CDLL

hello = CDLL('./hello.so')

if __name__ == '__main__':
    a = input('input num1: ')
    b = input('input num2: ')
    print('output: %d' % hello.add(int(a), int(b)))

運行上述代碼,得到輸出:

xujijun@pc:~/codespace/python$ python3 hello.py
input num1: 12
input num2: 34
output: 46

嘗試傳遞字符串參數

#include <iostream>
#include <cstdio>
using namespace std;

extern "C" {
    void print_name(const char* name) {
        printf("%s\n", name);
    }
}

Python代碼調用:

# -*- coding: utf-8 -*- #
from ctypes import CDLL

hello = CDLL('./hello.so')

if __name__ == '__main__':
    name = input('input name: ')
    hello.print_name(name.encode('utf-8'))  # 此處需要將Python中的字符串按照utf8編碼成bytes

查看輸出:

xujijun@pc:~/codespace/python$ python3 hello.py
input name: yanhewu
yanhewu

面向對象

使用C++編寫動態鏈接庫我們肯定會想到如何在Python中調用C++的類,由於ctypes只能調用C語言函數(C++中采用extern "C"聲明的函數),我們需要對接口進行一定的處理:

C++代碼示例

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;

class Student {
private:
    string name;    
public:
    Student(const char* n);
    void PrintName();
};

Student::Student(const char* n) {
    this->name.assign(n);
}

void Student::PrintName() {
    cout << "My name is " << this->name << endl;
}

extern "C" {
    Student* new_student(const char* name) {
        return new Student(name);
    }

    void print_student_name(Student* stu) {
        stu->PrintName();
    }
}

Python代碼中調用:

# -*- coding: utf-8 -*- #
from ctypes import CDLL

hello = CDLL('./hello.so')

class Student(object):
    def __init__(self, name):
        self.stu = hello.new_student(name.encode('utf-8'))
    
    def print_name(self):
        hello.print_student_name(self.stu)

if __name__ == '__main__':
    name = input('input student name: ')
    s = Student(name)
    s.print_name()

輸出:

xujijun@pc:~/codespace/python$ python3 hello.py
input student name: yanhewu
My name is yanhewu

內存泄漏?

上一部分我們我們嘗試了如何使用ctypes調用帶有類的C++動態庫,這裏我們不禁會想到一個問題,我們在動態庫中使用new動態申請的內存是否會被Python的GC清理呢?這裏我們完全可以猜想,C++動態庫中的動態內存並不是使用Python中的內存申請機制申請的,Python不應該對這部分內存進行GC,如果真的是這樣,C++的動態庫就會出現內存泄漏的問題了。那事實是不是這樣呢?我們可以使用內存檢查工具Valgrind來檢查上面的Python代碼:

命令:

valgrind python3 hello.py

最終結果輸出:

==17940== HEAP SUMMARY:
==17940==     in use at exit: 647,194 bytes in 631 blocks
==17940==   total heap usage: 8,914 allocs, 8,283 frees, 5,319,963 bytes allocated
==17940== 
==17940== LEAK SUMMARY:
==17940==    definitely lost: 32 bytes in 1 blocks
==17940==    indirectly lost: 0 bytes in 0 blocks
==17940==      possibly lost: 4,008 bytes in 7 blocks
==17940==    still reachable: 643,154 bytes in 623 blocks
==17940==         suppressed: 0 bytes in 0 blocks
==17940== Rerun with --leak-check=full to see details of leaked memory
==17940== 
==17940== For counts of detected and suppressed errors, rerun with: -v
==17940== Use --track-origins=yes to see where uninitialised values come from
==17940== ERROR SUMMARY: 795 errors from 86 contexts (suppressed: 0 from 0)

可以看到,definitely lost了32字節,確實出現了內存泄漏,但是是不是動態庫的問題我們還要進一步驗證:

C++代碼加入析構函數定義:

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;

class Student {
private:
    string name;    
public:
    Student(const char* n);
    ~Student();
    void PrintName();
};

Student::Student(const char* n) {
    this->name.assign(n);
}

Student::~Student() {
    cout << "Student's destructor called" << endl;
}

void Student::PrintName() {
    cout << "My name is " << this->name << endl;
}

extern "C" {
    Student* new_student(const char* name) {
        return new Student(name);
    }

    void print_student_name(Student* stu) {
        stu->PrintName();
    }
}

運行相同的Python代碼:

xujijun@pc:~/codespace/python$ python3 hello.py
input student name: yanhewu
My name is yanhewu

從輸出可以看到,Student的析構函數並沒有被調用。這裏可以確定,Python的GC並沒有對動態庫中申請的內存進行處理,也確實不能進行處理(畢竟不是Python環境下申請的內存,無法判斷是堆上的內存還是棧上的內存)。但是內存泄漏的問題還是需要解決的,可以參照以下做法:

C++代碼中添加內存釋放接口:

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;

class Student {
private:
    string name;    
public:
    Student(const char* n);
    ~Student();
    void PrintName();
};

Student::Student(const char* n) {
    this->name.assign(n);
}

Student::~Student() {
    cout << "Student's destructor called" << endl;
}

void Student::PrintName() {
    cout << "My name is " << this->name << endl;
}

extern "C" {
    Student* new_student(const char* name) {
        return new Student(name);
    }

    // 釋放對象內存函數
    void del_student(Student* stu) {
        delete stu;
    }

    void print_student_name(Student* stu) {
        stu->PrintName();
    }
}

Python代碼中,在Student類中調用內存釋放函數:

# -*- coding: utf-8 -*- #
from ctypes import CDLL

hello = CDLL('./hello.so')

class Student(object):
    def __init__(self, name):
        self.stu = hello.new_student(name.encode('utf-8'))

    def __del__(self):
        # Python的對象在被GC時調用__del__函數
        hello.del_student(self.stu)
    
    def print_name(self):
        hello.print_student_name(self.stu)

if __name__ == '__main__':
    name = input('input student name: ')
    s = Student(name)
    s.print_name()

運行Python代碼:

xujijun@pc:~/codespace/python$ python3 hello.py
input student name: yanhewu
My name is yanhewu
Student's destructor called

可以看到,C++動態庫中的Student類的析構函數被調用了。再次使用Valgrind檢查內存使用情況:

==23780== HEAP SUMMARY:
==23780==     in use at exit: 647,162 bytes in 630 blocks
==23780==   total heap usage: 8,910 allocs, 8,280 frees, 5,317,023 bytes allocated
==23780== 
==23780== LEAK SUMMARY:
==23780==    definitely lost: 0 bytes in 0 blocks
==23780==    indirectly lost: 0 bytes in 0 blocks
==23780==      possibly lost: 4,008 bytes in 7 blocks
==23780==    still reachable: 643,154 bytes in 623 blocks
==23780==         suppressed: 0 bytes in 0 blocks
==23780== Rerun with --leak-check=full to see details of leaked memory
==23780== 
==23780== For counts of detected and suppressed errors, rerun with: -v
==23780== Use --track-origins=yes to see where uninitialised values come from
==23780== ERROR SUMMARY: 793 errors from 87 contexts (suppressed: 0 from 0)

可以看到,definitely lost已經變為0,可以確定動態庫申請的內存被成功釋放。

使用ctypes在Python中調用C++動態庫