編程學習網 > 編程語言 > Python > python ctypes教程(C語言接口ctypes)
2023
07-29

python ctypes教程(C語言接口ctypes)

我們知道在Python中可以用os.popen()或subprocess.run()等方法調用其他編程語言生成的可執行文件或者系統命令,但是這種方式是文件級的調用,只能等指令運行結束才能知道執行結果,靈活度不高。這篇文章介紹的則是API級(函數級)的調用,用到Python標準模塊ctypes,ctypes提供了一種方法可以在Python代碼中調用C語言形式的API,需要注意的是ctypes并不支持C++形式的API,特別是C++中的類、重載等高級特性。


在C語言中函數調用包含幾個基本要素:1、函數名稱;2、入參;3、返回值,在Python中也是一樣包含了這3個基本要素。在ctypes中調用C語言的函數使用的函數名稱就是C語言的函數名稱,所以關鍵之處就在于入參和返回值怎么將Python的數據類型和C語言數據類型一一對應起來。

1、加載dll文件
要調用C語言寫的函數,首先需要在Python中加載動態鏈接庫,可以是自己編譯的動態鏈接庫也可以是系統的動態鏈接庫。在Windows中一般是dll文件或者pyd文件,在Linux系統中一般是so文件。

from ctypes import *

dll_obj = CDLL('some.dll')
#dll_obj = PyDLL('some.dll')
#dll_obj = WinDLL('some.dll')  # 只用于Windows
#dll_obj = OleDLL('some.dll')  # 只用于Windows
至于應該使用CDLL或者WinDLL還是OleDLL,是根據庫函數的約定調用方式決定的。CDLL是cdecl調用協議導出函數的加載方式,而 WinDLL是按照stdcall調用協議調用其中的函數。OleDLL也是按照stdcall調用協議調用其中的函數,并假定該函數返回的是Windows HRESULT錯誤代碼,當函數調用失敗時根據該代碼拋OSError異常。另外從源碼可以看出PyDLL方式則是繼承自CDLL:

class PyDLL(CDLL):
    """This class represents the Python library itself.  It allows
    accessing Python API functions.  The GIL is not released, and
    Python exceptions are handled correctly.
    """
    _func_flags_ = _FUNCFLAG_CDECL | _FUNCFLAG_PYTHONAPI
另外也可以使用xyzDll.LoadLibrary()方法打開鏈接庫文件:

from ctypes import *

dll_obj = cdll.LoadLibrary('some.dll')
#dll_obj = pydll.LoadLibrary('some.dll')
#dll_obj = windll.LoadLibrary('some.dll')  # 只用于Windows
#dll_obj = oledll.LoadLibrary('some.dll')  # 只用于Windows
通過上述2種方法生成了一個dll接口實例,就可以用“實例.函數()”的方式調用動態鏈接庫中的函數了。我們先看一個例子,調用標準C庫函數isupper()檢查某個字符是否為大寫,如果入參為大寫字符,返回的結果為非0值,反之為0,在Windows中大部分的標準C庫函數位于msvrt.dll中,采用CDLL的方式加載。C標準庫函數中isupper()的入參為int類型,所以這個例子中我們用ord()函數將字符轉換為int數據類型:

from ctypes import *

libc = CDLL('msvcrt.dll')       #加載dll
ret = libc.isupper(ord('A'))    #用對象名.函數名的方法調用c函數
print('isupper(\'A\'):',ret)
ret = libc.isupper(ord('a'))
print('isupper(\'a\'):',ret)
==========結果:
isupper('A'): 1
isupper('a'): 0
在C語言里如果函數類型為int型,形參傳入的即使是char型數據,會強制進行數據轉換,所以在C語言中isupper()函數可以直接輸入單個字符進行調用,我們試下像C語言里直接輸入字符看看效果:

from ctypes import *

libc = CDLL('msvcrt.dll')#加載dll
ret = libc.isupper('A')
print('isupper(\'A\'):',ret)
ret = libc.isupper('a')
print('isupper(\'a\'):',ret)
==========結果:
isupper('A'): 0
isupper('a'): 0
從上面的例子可以看到isupper()傳入大寫字母’A’居然都返回了0?。?!至于為什么返回的結果不正確,這里先挖個坑,后面再來解釋。



2、基本數據類型
在ctypes中,定義了多種數據類型,用來和C語言數據類型和Python常見數據類型相互對應,比如c_int類型對應了C語言的int和Python的int類型,c_double類型則對應了C語言的double和Python的float類型。

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 __int64 或 long long int
c_ulonglong
unsigned __int64 或 

unsigned long long

int
c_size_t size_t int
c_ssize_t ssize_t 或 Py_ssize_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
定義一個ctypes數據類型對象的方法:a=類型(值);或者先聲明對象類型再賦值:a=類型();a.value=值。

a = c_int(100)  #直接定義和賦值

a=c_int()    #先定義類型
a.value=100  #再賦值
2.1 c_int,c_long
from ctypes import *

a = c_int(100)
print('a=',a)
print('a.value=',a.value)
 
b = c_long(100)
print('b=',b)
print('b.value=',b.value)
==========結果:
a= c_long(100)  #定義的是c_int類型,但是打印出來的是c_long類型
a.value= 100
b= c_long(100)
b.value= 100
從上面的例子可以看出,變量a定義的是c_int類型,但是打印出來的是c_long類型,我們從ctypes的源碼中可以看到,如果int和long型的數據長度是一致的,c_int就是c_long的一個別名而已:

if _calcsize("i") == _calcsize("l"):
    # if int and long have the same size, make c_int an alias for c_long
    c_int = c_long
    c_uint = c_ulong
else:
    class c_int(_SimpleCData):
        _type_ = "i"
    _check_size(c_int)

    class c_uint(_SimpleCData):
        _type_ = "I"
    _check_size(c_uint)
在Python中int類型的數據理論上是可以無限大的,所以在傳入到C函數中時可能會進行截斷以適應C類型的整型長度,調用時需要注意這點,否則可能得到的不是自己想要的結果。

2.2 c_double
接下來看下c_double類型的數據,嘗試用標準C庫函數的pow(x,y)計算x的y次冪:

from ctypes import *

a = c_double(3.0)
b = c_double(2.0)

print('a=',a)
print('a.value=',a.value)
print('b=',b)
print('b.value=',b.value)

libc = CDLL('msvcrt.dll')   #加載dll

ret = libc.pow(a,b)         #用對象名.函數名的方法調用c函數
print('pow(a,b):',ret)
==========結果:
a= c_double(3.0)
a.value= 3.0
b= c_double(2.0)
b.value= 2.0
pow(a,b): 11
從這個例子可以看出并沒有得到預期的結果9,因為在ctypes中默認的是int類型,如果沒有按照C語言函數的實際類型聲明其返回值和入參類型,默認按照c_int類型傳入和返回,所以得到的結果是錯誤的。標準的做法是在調用這個函數前聲明其返回類型restype和入參類型argtypes,其中argtypes是一個tuple,需要注意的是即使只有一個入參也必須表示成tuple的形式:(arg1 , ) 其中組成tuple中的逗號是不能少的,否則會報“TypeError: argtypes must be a sequence of types”異常!

from ctypes import *

a = c_double(3.0)
b = c_double(2.0)

print('a=',a)
print('a.value=',a.value)
print('b=',b)
print('b.value=',b.value)

libc = CDLL('msvcrt.dll')

libc.pow.restype = c_double   #聲明返回類型
libc.pow.argtypes = (c_double, c_double) #####聲明入參類型
ret = libc.pow(a,b)
print('pow(a,b):',ret)
==========結果:
a= c_double(3.0)
a.value= 3.0
b= c_double(2.0)
b.value= 2.0
pow(a,b): 9.0
如上形式聲明C函數的入參和返回類型后,得到的結果就是正確的了。

C函數返回類型和入參類型聲明:
當函數的入參和返回值是非int類型時,調用這個函數前必須聲明其返回類型restype和入參類型argtypes,其中argtypes是一個tuple,需要注意的是即使只有一個入參也必須表示成tuple的形式:(arg1 , ) 。

2.3 c_float,c_longlong
前面利用標準C庫函數熟悉了c_int和c_double類型,其他的C語言基本數據類型也可以類比得出使用方法,接下來我們試著自己編譯dll文件來研究不同類型的入參和返回值,這部分內容需要對C語言有一定的了解,下面涉及到C語言部分的代碼在vs2015上編譯。

//C語言中定義了幾個類型的加法函數:
#ifdef __cplusplus
extern "C" {
#endif

__declspec(dllexport) int addi(int x, int y)
{
  return x + y;
}

__declspec(dllexport) double addd(double x, double y)
{
  return x + y;
}

__declspec(dllexport) float addf(float x, float y)
{
  return x + y;
}

__declspec(dllexport) long long addll(long long x, long long y)
{
  return x + y;
}

#ifdef __cplusplus
}
#endif
#Python中使用這些函數

from ctypes import *
pyt = CDLL('pytest.dll')   #加載dll

#int型的返回值和入參可以不聲明類型
ret = pyt.addi(55,22)
print('addi(55,22)=',ret)

#以下其他類型的返回值和入參必須聲明類型
pyt.addd.restype=c_double
pyt.addd.argtypes=(c_double,c_double)
ret = pyt.addd(5.55555555,2.22222222)
print('addd(5.55555555,2.22222222)=',ret)
 
pyt.addf.restype=c_float
pyt.addf.argtypes=(c_float,c_float)
ret = pyt.addf(5.55,2.22)
print('addf(5.55,2.22)=',ret)

pyt.addll.restype=c_longlong
pyt.addll.argtypes=(c_longlong,c_longlong)
ret = pyt.addll(5555555555555555555,2222222222222222222)
print('addll(5555555555555555555,2222222222222222222)=',ret)
==========結果:
addi(55,22)= 77
addd(5.55555555,2.22222222)= 7.77777777
addf(5.55,2.22)= 7.770000457763672
addll(5555555555555555555,2222222222222222222)= 7777777777777777777
2.4 字符或字符串數據
字符或字符串數據是一種比較特殊的存在,這里單獨拎出來講講。

//C語言函數,入參是char型,返回也是char型:
__declspec(dllexport) char trans_c(char y)
{
  return y ;
}
查對應關系表,C語言的char型數據,可以對應ctypes的c_char或者c_byte類型,分別對應到Python的bytes對象和int對象。所以可以寫成下面2種形式:

print('-----歡迎來到www.juzicode.com')
print('-----公眾號: 桔子code/juzicode\n')

from ctypes import *

pyt = CDLL('pytest.dll')   #加載dll

pyt.trans_c.restype=c_char
pyt.trans_c.argtypes=(c_char,)
ret = pyt.trans_c(b'A')   #因為對應Python的bytes對象,所以不能直接輸入'A'作為入參,必須使用b'A'作為入參。
print('trans_c(A)=',ret)


pyt.trans_c.restype=c_byte
pyt.trans_c.argtypes=(c_byte,)
ret = pyt.trans_c(ord('A')) #因為對應Python的int對象,所以不能直接輸入'A'作為入參,必須使用ord'A'轉換為int后作為入參。
print('trans_c(A)=',ret)
==========結果:
trans_c(A)= b'A'
trans_c(A)= 65
從上面的例子可以看出來,如果C語言函數是char型的數據類型,并不能直接用Python中的str類型數據直接作為入參傳遞給C函數,而是需要轉換為ctypes的bytes或者int類型。這也就是前面在Python中使用標準C庫函數isupper()直接用Python str對象’A’作為入參時為什么會得到錯誤結果的原因。

下面的例子看下使用wchar_t型數據作為入參時的情況,從前面數據類型的對應關系可以看到,可以使用Python單字符str對象作為入參傳遞給C函數的,同樣當使用多個字符組成的str作為入參時就會報錯了:

//C語言函數,入參是wchar_t 型,返回也是wchar_t 型:
__declspec(dllexport) wchar_t trans_wc(wchar_t y)
{
  return y;
}

from ctypes import *
pyt = CDLL('pytest.dll')   #加載dll

pyt.trans_wc.restype=c_wchar
pyt.trans_wc.argtypes=(c_wchar,)
ret = pyt.trans_wc('桔')
print('trans_wc(A)=',ret)

ret = pyt.trans_wc('桔子code')
print('trans_wc(A)=',ret)

==========結果:
trans_wc(A)= 桔

Traceback (most recent call last):
  File "E:\juzicode\py3study\m8-c接口\struct\ctypes-datatype-char.py", line 19, in <module>
    ret = pyt.trans_wc('桔子code')
ctypes.ArgumentError: argument 1: <class 'TypeError'>: wrong type
從前面的例子可以看到,c_char,c_byte,c_wchar都只能傳遞單個字符,當需要傳遞多個字符組成的字符串時,可以使用c_char_p和c_wchar_p指針作為傳遞對象:

__declspec(dllexport) char* trans_cp(char* y)
{
  return y;
}
__declspec(dllexport) wchar_t* trans_wcp(wchar_t* y)
{
  return y;
}
from ctypes import *
pyt = CDLL('pytest.dll')   #加載dll
pyt.trans_cp.restype=c_char_p
pyt.trans_cp.argtypes=(c_char_p,)
ret = pyt.trans_cp(b'juzicode')
print('trans_cp()=',ret)

pyt.trans_wcp.restype=c_wchar_p
pyt.trans_wcp.argtypes=(c_wchar_p,)
ret = pyt.trans_wcp('桔子code')
print('trans_wcp()=',ret)

==========結果:
trans_cp()= b'juzicode'
trans_wcp()= 桔子code


字符類型小結:
c_char: 用作單個字符傳遞,對應python bytes類型,用b’X’表示,對應c語言的char類型;
c_wchar: 用作單個寬字符傳遞,對應python str類型,直接用’X’傳遞,對應c語言的wchar_t類型;
c_char_p:用作字符串傳遞,對應python的bytes類型,用b’XYZ’表示,對應c語言的char*類型;
c_wcahr_p:用作字符串傳遞,對應python的bytes類型,直接用’XYZ’傳遞,對應c語言的wchar_t*類型;



3 數組類型
在ctypes中,基于前述的基本數據類型,還可以構建自定義的數組類型,自定義數組類型的方法:新類型名 = ctypes基本類型名*長度n,賦值方法:對象名=新類型名(賦值1,賦值2……賦值n) ;下面的例子是創建一個包含10個int類型的數組并且賦值:

c_int_10 = c_int * 10
arr_int = c_int_10(1,2,3,4,5,6,7,8,9,10)# 賦值的長度不能大于前面聲明的長度
print('arr_int:',arr_int)
for ar in arr_int:
  print(ar,end=' ')

==========結果:
arr_int: <__main__.c_long_Array_10 object at 0x0000018840C8FC48>
1 2 3 4 5 6 7 8 9 10
從上面的例子可以看出這個新定義的數據類型是可以迭代的。下面這個C函數的例子將包含10個元素的int數組作為入參傳入,在函數內部相加并返回和。聲明參數類型時就使用這個新的自定義參數類型,比如下面這個例子中C函數的入參為int x[10],在Python中的入參類型聲明是這樣的pyt.trans_array_i.argtypes=(c_int_10,):

__declspec(dllexport) long long trans_array_i(int x[10])
{
  long long sum = 0;
  printf("in c:");
  for (int i = 0; i < 10; i++)
  {
    printf("%d ", x[i]);
    sum += x[i];
  }
  printf("\n");
  return sum;
}

print('-----歡迎來到www.juzicode.com')
print('-----公眾號: 桔子code/juzicode\n')

from ctypes import *
#加載dll
pyt = CDLL('pytest.dll')
#定義新的數據類型
c_int_10 = c_int * 10
arr_int = c_int_10(1,2,3,4,5,6,7,8,9,10)# 賦值的長度不能大于前面聲明的長度
#聲明c函數的返回值和入參類型
pyt.trans_array_i.restype=c_longlong
pyt.trans_array_i.argtypes=(c_int_10,)
#調用c函數
ret = pyt.trans_array_i(arr_int)
print('trans_array_i()=',ret)

==========結果:
in c:1 2 3 4 5 6 7 8 9 10
trans_array_i()= 55
下面的例子中的函數,入參是一個包含20個元素的字符數組,函數的作用是打印字符數組的內容,并返回其下標為0的字符:

__declspec(dllexport) char trans_array_c(char x[20])
{
  for (int i = 0; i < 20; i++)
  {
    printf("%c ", x[i]);
  }
  printf("\n");
  return x[0];
}

from ctypes import *
#加載dll
pyt = CDLL('pytest.dll')
#定義新的數據類型
c_char_20 = c_char * 20
arr_char = c_char_20(b'j',b'u',b'z',b'i',b'c',b'o',b'd',b'e',b'.',b'c',b'o',b'm')
pyt.trans_array_c.restype=c_char
pyt.trans_array_c.argtypes=(c_char_20,)
#調用c函數
ret = pyt.trans_array_c(arr_char)
print('trans_array_c()=',ret)

==========結果:
j u z i c o d e . c o m
trans_array_c()= b'j'
這篇文章介紹了怎么在Python中加載動態鏈接庫文件,ctypes基本數據類型、數組類型以及入參和返回值的聲明方式,特別說明了容易掉坑的字符數據類型的注意點。接下來的文章將更深入介紹ctypes如何使用自定義數據類型、指針等內容。

小結:1.根據C函數的調用約定決定使用CDLL或者WinDLL加載動態鏈接庫;2.C函數入參或返回值如果是非int類型的數值,需要用argtypes和restype聲明數據類型,argtypes是一個元組;3.c_char和c_wchar只能傳遞單個字符的bytes和str類型,c_char_p和c_wchar_p可以用來傳遞多字符bytes和str。

以上就是python ctypes教程(C語言接口ctypes)的詳細內容,想要了解更多Python教程歡迎持續關注編程學習網。

掃碼二維碼 獲取免費視頻學習資料

Python編程學習

查 看2022高級編程視頻教程免費獲取