浅谈python中使用C/C++:ctypes

MamieEads 7年前
   <p><img src="https://simg.open-open.com/show/12afdcfa4368ee2a422f4bc8abf950cd.jpg"></p>    <h2><strong>前言</strong></h2>    <p><strong>python</strong> 这门语言,凭借着其极高的易学易用易读性和丰富的扩展带来的学习友好性和项目友好性,近年来迅速成为了越来越多的人们的首选。然而一旦拿python与传统的编程语言(C/C++)如来比较的话,人们往往会想到效率问题。本文不打算探讨语言之间的比较,然而python实际使用时确实会有能用更底层的C/C++更好的情况,因此本系列旨在介绍几种相对常见的 <strong>python环境下调用C/C++</strong> 的方法。(挖坑:CTYPES,SWIG,BOOST.PYTHON,CYTHON)</p>    <h2><strong>阅读这篇文章需要什么?</strong></h2>    <p>语言:简单的python基础与简单的C/C++基础。</p>    <p>C/C++的环境与python的环境。</p>    <p>步步跟进</p>    <p>搜索引擎/工具书 随时查询不明白的地方。</p>    <p>PS:本文中会有一些延伸性的知识点,加之本人语文水平惨不忍睹导致文风惊悚,所以 <strong>如果阅读途中感到不适请务必跳过延伸性的部分(用大括号括起来的部分)</strong> 。</p>    <p>目录</p>    <ul>     <li> <p>一、环境配置</p> </li>     <li> <p>二、C/C++一侧</p>      <ul>       <li> <p>库</p> </li>       <li> <p>代码</p>        <ul>         <li> <p>extern "C"</p> </li>         <li> <p>extern 和 static</p> </li>         <li> <p>#ifdef</p> </li>         <li> <p>DLL_EXPORT</p> </li>         <li> <p>__cdecl和__stdcall</p> </li>        </ul> </li>      </ul> </li>     <li> <p>三、CTYPES</p>      <ul>       <li> <p>加载</p> </li>       <li> <p>数据类型</p> </li>       <li> <p>访问导出变量</p> </li>       <li> <p>函数进出参数的定义</p>        <ul>         <li> <p>argtypes</p> </li>         <li> <p>restype</p> </li>        </ul> </li>       <li> <p>指针和引用</p> </li>       <li> <p>数组</p> </li>       <li> <p>小结</p> </li>      </ul> </li>     <li> <p>四、参考资料</p> </li>    </ul>    <h2><strong>一、环境配置</strong></h2>    <p>介于这是本系列的第一篇,我简单介绍一下环境(vim+命令行的大佬们可以跳了……):</p>    <p>python:可以从[python官网]( <a href="/misc/goto?guid=4959723473763745353" rel="nofollow,noindex"> https://www. python.org </a> )下载(不KX上网好像有点难开……?)。现在选择2还是3这个问题……依然是各有说法。然而可以两个都装因此也无所谓啦。IDE则是python默认的。PS:要注意一定要和C那边的位数匹配!如果GCC是32位的,用64位python就会报错,血的教训……</p>    <p>C/C++:我用的IDE是[codeblock]( <a href="/misc/goto?guid=4959723473849687512" rel="nofollow,noindex"> Code::Blocks </a> )——一个跨平台的开源C/C++集成开发环境。建议下载mingw-setup版,自带GCC/G++和GDB debugger。另外如果需要VC编译的话还需要下载VC并且在codeblock的compiler settings里面调整路径。</p>    <p>现在作为一个python高手和C高手的李狗蛋从python官网上下载了python3.5 32-bit和codeblock mingw-setup,他胸有成竹,因为他明白他的命运,他是宇宙中排名unsigned int(-1)的程序员,是超越最强的男人(并不)。他兴奋的准备写出一个可以征服世界的程序。</p>    <h2><strong>二、C/C++一侧</strong></h2>    <p>ctypes的使用是通过调用C/C++的动态链接库(DLL)实现的,因此在进入正题之前,还要先讲讲动态链接库的构建方式。这一块会牵扯到各种编译器和系统和语言相关的问题,本文只讨论我们目前所需要了解的部分。</p>    <h2><strong>库</strong></h2>    <p><strong>库的本质</strong> 就是一个打包好的代码包,一般分为静态(.lib .a)和动态(.dll .so)。静态库在主程序编译时就会被一并编译到最终的可执行文件中,然而python并没有编译这个过程,python主要使用的是动态库,即在运行时再去库里找内容。</p>    <p>对于使用IDE的人来说,要建一个库只需要在新建项目时选择项目属性为库即可。</p>    <p>{</p>    <p>而对于命令行的使用者,则需要在编译时添加一些命令(以C++为例,C把g++改成gcc即可):</p>    <pre>  <code class="language-python">g++ -fPIC -shared -o libsource.so Source.cpp</code></pre>    <p>其中-shared代表这是动态库,-fPIC使得位置独立,如果程序本来就是独立的话会有警告,无视即可) -o指定了输出文件,改成dll后缀一样可以用。</p>    <p>}</p>    <p>如果要使用C/C++调用这个库则还需要许多繁琐的流程,好在我们是用python调用,所以可以到此为止。</p>    <p>李狗蛋看了以后,运用他极高的编程技术,稳定的操控着鼠标,依次点击了code:block菜单栏上的File-New-project-Dynamic Link Library,并且一路按着下一步,动作有如雷霆一般,好似忘我的舞者。忽然,他发现对话框卡在了一个地方,原来是要给程序命名。他略微沉思了一会,就毅然打下了一行英文:SSR 似乎是为了弥补生活中的空缺。</p>    <h2><strong>代码</strong></h2>    <p>弹出来的窗口里充斥着许多不知名的符号,李狗蛋吃了一惊,心里道:呵!有意思,居然让本大爷愣了1s,好汉不吃眼前亏,我先来学习一个。</p>    <p>假如你们像我说的那样在IDE中新建了一个动态库project,你们多多少少会被蹦出来的样板文件闹得有点不知所措(本文假设读者是和我一样的只写过十分简单的C/C++文件的人)。这不奇怪,为了各种方面的兼容性,一个文件如果要按照项目的标准来写,会有一些个人独自写小文件时用不上的命令。比如:</p>    <p><strong><u>extern "C"</u> </strong></p>    <p>由于C++多了函数重载的功能,导致了一个函数不能仅仅用它的名字来确定。</p>    <p>其结果就是编译以后函数名(如`git`)往往会变成像`_Z3gitv`这种样子(不同编译器变法还有些不同)。那导致的直接原因就是难以调用。而这就到了`extern "C"`命令出场的时候了,该命令告诉编译器用C的方式编译这一句,而C的函数名不会改变,于是编译后可以直接用函数名调用。对我们来说几乎每一个C++的函数都要加上extern "C"。</p>    <p>/*函数重载:比如函数 int fun(int a)和int fun(double a),在C的眼里是同一个函数(名字一样),在C++眼里是不同的函数(名一样,姓氏,性别,字号不一样啊)。理论上C++的更加科学,但是为了区分,它会对函数进行重命名。比如对李狗蛋:LJ_李狗蛋_男_84_82_86_单身,这样的结果就是我说李狗蛋,它不知道是谁,只知道有个名字很长的函数。所以我们需要extern "C"来告诉它不要乱给别人改名,会被揍的。*/</p>    <p><strong><u>extern和static</u> </strong></p>    <p>看到标题很多人都会想:这里的extern和上文有关系吗?……</p>    <p>答案是 有,也没有。要解释的话我们就要简单了解一下这两个关键词了。</p>    <p>{</p>    <p>static对于局部变量来说意为静态变量,即不随该函数的生死而产生或消亡。当然这是题外话了,我们要谈的是另一方面的static。</p>    <p>}</p>    <p>对于 <strong>全局变量/函数定义</strong> 来说,extern和static是一对 <strong>反义词</strong> 。</p>    <p>extern指示内容不仅仅限于本文件,可能在别的文件里被定义/被调用,而static则表示该元素仅仅允许在本文件中使用。上面命令(extern "C")的本质是“ <em>是extern的,同时是C的</em> ”。</p>    <p>{</p>    <p>然而由于"C"事实上只有这一种用法,而且只有函数需要被外部调用时我们才关注它是否被改名了,所以事实上形成了一种类似英语中的固定搭配的用法,又没有关系了。</p>    <p>}</p>    <p>在我们的使用方面,可以用static来形成类的private部分的效果。</p>    <p>/* 也就是说,假如我们写了一个函数叫getMyDarkDragonEyeOn(),但我们不想被外部调用,那我们就用static声明这个东西我要偷偷藏起来不给别人用。但是如果别人调用一个extern的函数ZhongErSoul() ,这个函数是可以调用static函数的(因为他是内部)。变量同理,我们可以声明static finalexamScore来避免被直接访问到不想被访问的变量。*/</p>    <p>DLL_EXPORT(如果用VC编译必读)</p>    <p>对于GCC编译来说,所有函数默认(未加static)都是导出的,即可以被外部调用的。然而对于VC来说就不一样了, <strong>VC默认不导出</strong> ,所以我们需要这样一段:</p>    <pre>  <code class="language-python">#ifdef _MSC_VER          #define DLL_EXPORT __declspec(dllexport)       #else          #define DLL_EXPORT</code></pre>    <p>其中_MSC_VER 是VC的一个宏定义,从而检测编译器是否是VC而决定是否使用__declspec( dllexport )</p>    <p>然后这样写函数</p>    <pre>  <code class="language-python">extern "C" DLL_EXPORT int function()</code></pre>    <p>{</p>    <p><strong><u>cdecl与__stdcall</u> </strong></p>    <p>这是两种不同的调用方式,涉及到编译和底层汇编的一些细节,这里不作展开。C/C++默认的都是使用__cdecl,可通过在函数前面添加关键词定义该函数的调用方式,我们可以不用管。</p>    <p><strong><u>#ifdef</u> </strong></p>    <p>许多这种类似的语句为宏,一般样板里的都是为了避免重复定义而存在。意为“如果定义了……那么……”,类似的还有一些,可以自行了解。</p>    <p>}</p>    <p>对于我们来说,(如果用GCC)只要在写出来的文件里加extern "C"(可能还要static)就够了。</p>    <p>读完以后,李狗蛋浑身充满了能量,他打算在这个项目里祭出他最强的一段代码,以感谢文章的作者。于是他写了这么一段:</p>    <pre>  <code class="language-python">#include<cstdio>  extern "C" void saikyo()  {       printf("Hello world!");  }</code></pre>    <p>手起刀落,狗蛋满意的笑了。</p>    <h2><strong>三、CTYPES</strong></h2>    <h2><strong>加载</strong></h2>    <p>首先毫无疑问 我们要import ctypes。</p>    <p>ctypes有CDLL和WinDLL两种调用方式,对应上面说过的__cdecl和__stdcall,我们一般使用CDLL。</p>    <p>windows下会自动补充.dll后缀,而Linux则需要包含扩展名在内的全名才可调用</p>    <p>要加载一个dll,我们有许多方法:</p>    <pre>  <code class="language-python">cdll.filename  cdll.LoadLibrary("filename")  CDLL("filename")</code></pre>    <p>这三个都会调用filename.dll(Windows下自动补充后缀)并返回一个句柄一样的东西,我们便可以通过该句柄去调用该库中的函数。如:</p>    <pre>  <code class="language-python">cdll.study.math()</code></pre>    <p>也可以把句柄保存下来使用:</p>    <pre>  <code class="language-python">h = cdll.study      h.math()</code></pre>    <p>{</p>    <p>这里还有一个用法,由于dll导出的时候会有一个exports表,上面记录了哪些函数是导出函数,同时这个表里也有函数的序号,因此我们可以这样来访问表里的第一个函数</p>    <pre>  <code class="language-python">cdll.study[1]</code></pre>    <p>(要注意是从1而非0开始编号) 返回的是该函数的指针一样的东西,如果我们要调用的话就在后面加(parameter)即可。</p>    <p>关于exports表,GCC似乎编译时会自动生成def,可以在里面查,如果只有DLL的话,可以用VC的depends,或者dumpbin来查。</p>    <p>}</p>    <h2><strong>数据类型</strong></h2>    <p>李狗蛋在dll文件所在的目录下建了一个py文件,然后用IDLE打开并run python shell,反复使用上一小节所学的的技能输出了几十行Hello World,旁边的张小花不由得露出了心心眼。一旁的王小锤却嗤笑一声:“有什么好嚣张的,它能做数学题吗?”李狗蛋听完心里不由得一股怒火,居然敢挑战狗王的权威,今天就让你看看什么叫老司机。于是他又加了一个函数,还不忘加上新学的extern "C":</p>    <pre>  <code class="language-python">extern "C" int hardestproblem(int a,int b)  {       return a+b;  }</code></pre>    <p>/*None, integers, bytes objects and (unicode) strings are the only native Python objects that can directly be used as parameters in these function calls. None is passed as a C NULL pointer, bytes objects and strings are passed as pointer to the memory block that contains their data (char * or wchar_t *). Python integers are passed as the platforms default C int type, their value is masked to fit into the C type.*/</p>    <p>上面的摘自python官方文档。描述了几种 <strong>可以直接传递</strong> 的参数类型及其在C一侧的形式。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/a54f312101cef3f2e47305a77a7a9c3a.png"></p>    <p>狗蛋在python里输入了以下内容</p>    <pre>  <code class="language-python">from ctypes import *  god = cdll.god  god.hardestproblem(1,2)</code></pre>    <p>得到了输出3,完美的解决了小锤的问题。</p>    <p>对于我们来说,再加上浮点值几乎就够一般使用了。然而浮点值不是默认可传递的,于是我们需要进行类型转换:</p>    <pre>  <code class="language-python">c_float(3.14)#单精度浮点类型      c_double(3.14)#双精度浮点类型</code></pre>    <p>定义变量后,用i.value访问值。</p>    <p>小锤似乎还想说些什么,然而早已被狗蛋看穿:“你想让我求圆的面积吧,哼哼。你以为问个这么尖端的问题我就不会答了?看着。于是在C++源文件中加了一个函数:</p>    <pre>  <code class="language-python">extern "C" double circle(double pi,double r)  {        return r*r*pi;  }</code></pre>    <p>运用刚刚学的转换语句,他在python里打道:</p>    <pre>  <code class="language-python">god.circle(c_double(3.14),c_double(1))</code></pre>    <p>然而似乎命运总是会捉弄主角一下,屏幕显示出了18805556,狗蛋的脸渐渐变得铁青……</p>    <p>狗蛋遇到的问题先放放,单从参数类型来看,似乎这样就够我们使用了。然而这其中需要注意一下字符串,字符串由于编码等等会有不少问题,比如:</p>    <p>str与bytes之间的编码问题,python默认的字符串str是unicode编码,占用两个字节。因此如果你只是用双引号写一个字符串传到函数里,如果你函数里又需要修改/读取指定的位,那么就会发生奇怪的事情,其原因是unicode的两字节与Char的一字节不匹配。详细可以参考 字符串和编码</p>    <p>为了解决这个问题,我们还要加`b""`让他强制生成bytes类型。</p>    <p>或者更安全的方法是:用create_string_buffer()生成一个更类似与C字符数组的东西以便操作安全。如:</p>    <pre>  <code class="language-python">create_string_buffer(b"abcdefg",10)</code></pre>    <p>来开一个长度为10的前面部分是abcdefg后面用NUL补齐的字符数组</p>    <p>另外:C中printf的输出只会出现在stdout(即命令行里),不会出现在IDLE里。</p>    <p>为了挽回小花的芳心,狗蛋回家后仔细研读了本教程,写了一个很浪漫的程序发给小花:</p>    <pre>  <code class="language-python">extern "C" int password(char *A,int n)  {       printf("%c",A[n])  }</code></pre>    <p>然后在让小花在python中输入以下内容</p>    <pre>  <code class="language-python">from ctypes import *  god = cdll.god  god.password(b"Live",0)  god.password(b"cool",1)  god.password(b"Have",2)  god.password(b"None",3)</code></pre>    <p>结果我们都能够猜到:小花十分感动…………………………………………然后拒绝了他。</p>    <p><strong>详细的类型名称表格见文章后面附图。</strong></p>    <p>{</p>    <p>如果你需要用自定义的数据类型来当作参数传递的话,需要参数中有个名为_as_parameter_的变量,ctypes会去找这个名字的变量来当作参数传递。涉及自定义类的内容不作展开。</p>    <p>}</p>    <h2><strong>访问导出变量</strong></h2>    <p>和函数一样,dll中的导出变量也可以被外部访问。格式如下:</p>    <pre>  <code class="language-python">c_int.in_dll(study,"score")</code></pre>    <p>其中c_int表示数据类型,in_dll表示在dll内,study处是dll名,后面的字符串是变量名。要注意变量和函数一样, <strong>不加extern"C"的话会被编译器改名</strong> 。</p>    <h2><strong>函数的进出参数的定义</strong></h2>    <p><strong>argtypes</strong></p>    <p>python并不会直接读取到dll的源文件,那么如何告诉python,函数需要什么参数呢?答案是用argtypes。</p>    <pre>  <code class="language-python">fuction.argtypes = [c_char_p,c_int]</code></pre>    <p>这样,在后面你使用fuction时python会自动处理你的参数,从而达到像调用python参数一样。</p>    <p><strong>restype</strong></p>    <p>和上面一个一样,python不仅看不到函数要什么,也看不到函数返回什么。默认情况下,python认为函数返回了一个C中的int类型。然而如果我们的函数返回别的类型,就需要用到restype命令。</p>    <pre>  <code class="language-python">function.restype = c_char_p</code></pre>    <p>指定了fuction这个函数的返回值是c_char_p的,从而让python在处理时按照我们希望的那样处理。</p>    <p>我们甚至可以设置函数的返回值为一个python对象(比如函数),目标函数执行后返回int并用该int直接调用该python对象。这里不作展开。</p>    <p>话说狗蛋被拒绝后伤心欲绝,功力大失,遂进网吧修行10年,读到了这一段,如醍醐灌顶。于是约了小锤午夜时分,情人谷上见。时辰到了,只见小锤骑着他的自行车,后座载着小花晃晃悠悠上山来了。</p>    <p>“小锤!十年前你害得我失去一切的问题,我终于解出来了!我不怕事的python扛把子今天要告诉你,美人,只配强者拥有!</p>    <p>话音未落 ,狗蛋拿出他早已写好的程序,C部分没有改动,然而python部分,却确确实实不一样了。</p>    <pre>  <code class="language-python">circle = god.circle  circle.argtypes = [c_double,c_double]  circle.restype = c_double  circle(3.14,1)</code></pre>    <p>只见屏幕闪耀着金光(狗蛋视角),蹦出了那个令狗蛋浑身一震的数字:3.14。</p>    <p>狗蛋一愣,随后是止不住的狂喜,他苦苦追求了这么多年,总算有了结果。据凌云上的同学说,半夜听到有人一直在哈哈哈哈哈哈,大家都准备抄家伙了。</p>    <p>小锤和小花对视数秒,讪讪的说:那……没事我们先下去了啊……点了鸡排。</p>    <p>于是,上来不过数分钟,两人又骑着车冲下了山,抱的紧紧的。而狗蛋的笑声迟迟不散,却多了几分凄凉……</p>    <p>{</p>    <h2><strong>指针和引用</strong></h2>    <p>指针和引用是非常常用的(特别在C中),这里不进行介绍,只讲讲用法。</p>    <pre>  <code class="language-python">byref(i)      pointer(i)</code></pre>    <p>分别产生i的引用和i的指针,其中如果不必要使用指针的话,引用会更快。可以通过输出指针来观察指针的属性。</p>    <h2><strong>数组</strong></h2>    <p>ctypes重载了*,因此我们可以用类型*n来表示n个该类型的元素在一起组成一个整体。如定义整数数组类型:</p>    <pre>  <code class="language-python">int_10 = c_int *10      myarr = int_10()      myarr[2]=c_int(24)</code></pre>    <p>传到C那边函数形参就应该是(int a[ ]),多维数组同理。</p>    <p>}</p>    <h2><strong>小结</strong></h2>    <p>到现在我们应该学会了怎么创建dll,怎么用ctypes加载dll,访问里面的函数和变量。传一些基本的参数进去并接收一些返回值。ctypes还有一些关于structure,union等的用法,就不一一阐述了。(其实是我也不怎么会python……)现在学会的内容应该足够支持起普通的C/C++调用了。</p>    <p>后来有一天在机房,一个颓废的中年男子走了进来,开了一台偏僻的机器,问我:ctypes的dll载入有4种写法,你知道吗?</p>    <p>----------</p>    <p>(完整的类型对应表格摘录如下:)(吐槽知乎居然不能用markdown……)</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/15e4544d5882894f26869c5943e52707.png"></p>    <p>----------</p>    <h2><strong>四、参考资料</strong></h2>    <p>1、extern "C"的用法解析 - Rollen Holt - 博客园: <a href="/misc/goto?guid=4959723473941950147" rel="nofollow,noindex"> http://www. cnblogs.com/rollenholt/ archive/2012/03/20/2409046.html </a></p>    <p>2、C++静态库与动态库 - 吴秦 - 博客园: <a href="/misc/goto?guid=4959723474022874852" rel="nofollow,noindex"> http://www. cnblogs.com/skynet/p/33 72855.html </a></p>    <p>3、__stdcall,__cdecl,__fastcall的区别 - 学无止境 - 博客频道 - <a href="/misc/goto?guid=4959723474105329879" rel="nofollow,noindex"> http:// CSDN.NET </a> : <a href="/misc/goto?guid=4959723474199110708" rel="nofollow,noindex"> http:// blog.csdn.net/kiki113/a rticle/details/4971886 </a></p>    <p>4、聊聊Python ctypes 模块 - 蛇之魅惑 - 知乎专栏: <a href="/misc/goto?guid=4959723474290109608" rel="nofollow,noindex"> https:// zhuanlan.zhihu.com/p/20 152309?columnSlug=python-dev </a></p>    <p>5、python 3 documentation: <a href="/misc/goto?guid=4959723474383782747" rel="nofollow,noindex"> https://docs.python.org/3/library/ctypes.htmlshi </a></p>    <p>6、字符串和编码 <a href="/misc/goto?guid=4959723474470878789" rel="nofollow,noindex"> http://www. liaoxuefeng.com/wiki/00 14316089557264a6b348958f449949df42a6d3a2e542c000/001431664106267f12e9bef7ee14cf6a8776a479bdec9b9000 </a></p>    <p> </p>    <p>来自:https://zhuanlan.zhihu.com/p/23372572</p>    <p> </p>