Table of Contents
2024年12月总结
时隔一年多才想起这篇笔记已经冷藏很久了,我甚至已经忘记swig是什么东西。上网一搜才发现我一年前学的swig可能是最难用的一种python c wrapper:
🔗 [SWIG | Python进阶] https://eastlakeside.gitbook.io/interpy-zh/c_extensions/swig
实际上现在写python c/c++ wrapper应该用的是ctypes和原生API:
以后要用到了再说吧。这玩意在过去的一年里只用到了一次,而且学完swig以后发现大部分情况下的运行效率还不如纯python高。
swig的编译与运行(macOS和Linux)
原始方法:直接用python-dev和cython把python代码转换成.c代码,然后上gcc
缺点是难学难搞
现在需要用c/c++写核心的计算库,然后在python中调用
🔗 [SWIG之为C/C++的API生成Python调用接口基础 | Walker's Blog] http://walkerdu.com/2017/12/06/swig-basic/
能在ubuntu系统上调通;暂时无法在macOS上调通(出segment fault,怀疑是bugOS的问题)
突然就能在macOS上面跑通了,关键教程是需要bundle OSX developer library -lSystem :
🔗 [SWIG tutorial for Mac OS X] https://expobrain.net/2011/01/23/swig-tutorial-for-mac-os-x/
🔗 [assembly - nasm - Can't link object file with ld on macOS Mojave - Stack Overflow] https://stackoverflow.com/questions/52830484/nasm-cant-link-object-file-with-ld-on-macos-mojave
Example:
swig -c++ -python example.i
clang++ -c -fpic -I/usr/local/Caskroom/miniconda/base/envs/py39/include/python3.9 example.c example_wrap.cxx
clang++ -L/usr/local/Caskroom/miniconda/base/envs/py39/lib -lpython3.9 -dynamiclib -shared example.o example_wrap.o -o _example.so
ld -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem -bundle -flat_namespace -undefined suppress -o _example.so *.o
各种传递参数/返回参数的简单代码
未解决的问题:python传递string -> const char *
但可以先放在一边
差不多(尝鲜)得差不多了,该系统的学一学这些东西了:
- 各种参数传递的情况(比如python传递List过去(特别是那种List of Tuple of List的嵌套结构),C++返回指针或结构体回来,等等)
- C++的矩阵计算
- 如何把大量数据一次性传递过去进行计算以节省时间(目前认为swig每调用一次C++代码就有相当大的时间开销)
对下面的程序的注明:
下面的代码如果cpp文件里有main函数,那么cpp和python-swig都能跑
跑python-swig的脚本(-std=c++11有的时候是-std=c++17):
swig -c++ -python example.i
clang++ -std=c++11 -c -fpic -I/usr/local/Caskroom/miniconda/base/envs/py39/include/python3.9 example.cpp example_wrap.cxx
clang++ -std=c++11 -L/usr/local/Caskroom/miniconda/base/envs/py39/lib -lpython3.9 -dynamiclib -shared example.o example_wrap.o -o _example.so
ld -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem -bundle -flat_namespace -undefined suppress -o _example.so *.o
python3 run.py
单独编译并运行cpp文件的脚本:
clang++ -std=c++17 -I/usr/local/Caskroom/miniconda/base/envs/py39/include/python3.9 example.cpp -o example
chmod u+x example
./example
在进行下一步学习之前,先把现有的代码贴出来:
python传入int,C++返回int
example.cpp
// example.cpp
#include <Python.h>
int fact(int n) {
if (n <= 1)
return 1;
else
return n*fact(n-1);
}
example.i
// example.i
%module example
%{
extern int fact(int);
%}
extern int fact(int);
run.py
import example
print(example.fact(4))
运行结果是24 .
接下来从这个基本的例子出发一点一点增加新的试验功能:
python传入(1个)int,C++返回2个int
使用数据结构pair_v
example.cpp
// example.cpp
#include <Python.h>
#include<iostream>
using namespace std;
struct pair_v {
int v1;
int v2;
};
pair_v return_two_values(int a, int b) {
pair_v pv;
pv.v1 = a + b;
pv.v2 = a - b;
return pv;
}
int main(int argc, char const *argv[]) {
pair_v pv = return_two_values(10, 9);
cout << pv.v1 << ", " << pv.v2 << endl;
}
example.i
// example.i
%module example
%{
struct pair_v{
int v1;
int v2;
};
extern pair_v return_two_values(int, int);
%}
struct pair_v{
int v1;
int v2;
};
extern pair_v return_two_values(int, int);
run.py
import example
pair_v = example.return_two_values(10, 9)
print(type(pair_v))
print(pair_v.v1)
print(pair_v.v2)
注意运行结果(仅运行python),返回的并不是tuple或者list,而是class pair_v:
<class 'example.pair_v'>
19
1
string类型的传递
python传入string,C++返回string
由于近几年基本上只学过C,所以紧急补一下 🔗 [C++ 字符串 | 菜鸟教程] https://www.runoob.com/cplusplus/cpp-strings.html
参考了一点点🔗 [SWIG Library] https://www.swig.org/Doc3.0/Library.html#Library_std_string
关键在于example.i文件中的 %include "std_string.i"
example.cpp
// example.cpp
#include <Python.h>
#include<iostream>
using namespace std;
string handle_str(string str) {
return str + str;
}
int main(int argc, char const *argv[]) {
cout << handle_str("google") << endl;
}
example.i
// example.i
%module example
%include "std_string.i"
%{
extern std::string handle_str(std::string str);
%}
extern std::string handle_str(std::string str);
run.py
import example
print(example.handle_str("google"))
结果(仅运行python):
googlegoogle
python传入string,C++返回string和int
本质上是练习,没有加入新的知识
example.cpp
// example.cpp
#include <Python.h>
#include<iostream>
using namespace std;
struct complex {
string v1;
int v2;
};
complex handle_str(string str) {
complex c;
c.v1 = str + str;
c.v2 = 10;
return c;
}
int main(int argc, char const *argv[]) {
complex c = handle_str("google");
cout << c.v1 << ", " << c.v2 << endl;
}
example.i
// example.i
%module example
%include "std_string.i"
struct complex{
std::string v1;
int v2;
};
%{
struct complex{
std::string v1;
int v2;
};
extern struct complex handle_str(std::string str);
%}
extern struct complex handle_str(std::string str);
run.py
import example
c=example.handle_str("google")
print(c.v1)
print(c.v2)
结果(仅运行python):
googlegoogle
10
python传入int,C++返回一堆int,不能使用struct封装
现在要开始使用vector了!
参考了一点:🔗 [SWIG Library] https://www.swig.org/Doc2.0/Library.html#Library_std_vector
example.cpp
// example.cpp
#include <Python.h>
#include<iostream>
using namespace std;
vector<int> many_integers(int len) {
vector<int> obj;
for (int i = 0; i < len; i++) {
obj.push_back(i);
}
return obj;
}
int main(int argc, char const *argv[]) {
vector<int> obj = many_integers(10);
for (int a: obj) {
cout << a << endl;
}
}
example.i
// example.i
%module example
%include "std_vector.i"
namespace std {%template(vectori) vector<int>;};
%{
extern std::vector<int> many_integers(int len);
%}
extern std::vector<int> many_integers(int len);
run.py
import example
obj=example.many_integers(10)
print(type(obj))
print(len(obj))
for a in obj:
print(a)
注意,如果example.i没有这一段:
namespace std {%template(vectori) vector<int>;};
就会导致
<class 'SwigPyObject'>
swig/python detected a memory leak of type 'std::vector< int,std::allocator< int > > *', no destructor found.
并且无法正确打印返回的数值
boost variant类型就先跳过了,暂时没有这个硬性需求(可以通过多写变量绕过)
python传入list of string,C++返回int
C++返回什么其实无所谓,主要是如何传入list of string比较重要
注意:在下面的代码中,python不仅可以传入list of string,还可以传入tuple of string(不需要修改C++代码)
example.cpp
// example.cpp
#include <Python.h>
#include<iostream>
using namespace std;
string combo_str(vector <string> input) {
string res = "";
for (string str: input) {
res += str;
}
return res;
}
int main(int argc, char const *argv[]) {
cout << combo_str({"abc", "def"}) << endl;
}
example.i
// example.i
%module example
%include "std_string.i"
%include "std_vector.i"
namespace std {%template(vectori) vector<std::string>;};
%{
extern std::string combo_str(std::vector<std::string> input);
%}
extern std::string combo_str(std::vector<std::string> input);
run.py
import example
print(example.combo_str(["abc","def","ghi","jklmn","opq","rs","t"]))
print(example.combo_str(("abc","def","ghi","jklmn","opq","rs","t"))) #结果一样
注意1:直接参数传入vector的写法 combo_str({"abc", "def"}); 需要C++11以后的编译参数 -std=c++11 :
swig -c++ -python example.i
clang++ -std=c++11 -c -fpic -I/usr/local/Caskroom/miniconda/base/envs/py39/include/python3.9 example.cpp example_wrap.cxx
clang++ -std=c++11 -L/usr/local/Caskroom/miniconda/base/envs/py39/lib -lpython3.9 -dynamiclib -shared example.o example_wrap.o -o _example.so
ld -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lSystem -bundle -flat_namespace -undefined suppress -o _example.so *.o
python3 run.py
注意2:
如果试图在python传入的list of string里面混入其他类型的数据(比如int, float),就会被发现并报错
再次挑战variant
在chatgpt的帮助下先写了一个c++的demo:
// example.cpp
// #include <Python.h>
#include <iostream>
#include <variant>
using namespace std;
vector <std::variant<int, double, std::string>> combo_variants(vector <std::variant<int, double, std::string>> input) {
vector <std::variant<int, double, std::string>> comb;
double d_sum = 0;
int i_sum = 0;
string str_comb = "";
for (const std::variant<int, double, std::string> &obj: input) {
if (std::holds_alternative<int>(obj)) {
// std::cout << "It's an int: " << std::get<int>(obj) << std::endl;
i_sum += std::get<int>(obj);
} else if (std::holds_alternative<double>(obj)) {
// std::cout << "It's a double: " << std::get<double>(obj) << std::endl;
d_sum += std::get<double>(obj);
} else if (std::holds_alternative<std::string>(obj)) {
// std::cout << "It's a string: " << std::get<std::string>(obj) << std::endl;
str_comb += std::get<std::string>(obj);
}
}
comb.push_back(i_sum);
comb.push_back(d_sum);
comb.push_back(str_comb);
return comb;
}
int main(int argc, char const *argv[]) {
vector <std::variant<int, double, std::string>> comb = combo_variants(
{"abc", "def", 10.1, 10.2, 100, 200, "ghi", "10.3", 300});
for (const auto &obj: comb) {
std::visit([](const auto &value) {
std::cout << value << std::endl;
}, obj);
}
}
运行结果:
600
20.3
abcdefghi10.3
但是问题来了,写完以后才发现swig不支持std::variant(可能是因为c++17才引入的variant有点新)
所以这个问题又只能先摆一边了,等以后系统学了一些c++技巧再来试试吧
继续:
python传入List of List of int,C++返回List of List of int
Example: [[1,2,3,4],[2,3,4],[3,4]] -> [[10],[9],[7]]
关键部分:在example.i中加入正确的声明: namespace std {%template(v_int) vector<int>;%template(v_v_int) vector<vector<int>>;};
example.cpp
// example.cpp
#include <Python.h>
#include<iostream>
using namespace std;
vector <vector<int>> sum_subVector(vector <vector<int>> input) {
vector <vector<int>> res;
for (vector<int> subV: input) {
int sum = 0;
for (int v: subV) {
sum += v;
}
res.push_back({sum});
}
return res;
}
int main(int argc, char const *argv[]) {
vector <vector<int>> res = sum_subVector({{1, 2, 3},
{2, 3, 4},
{10, 10}});
for (vector<int> subV: res) {
cout << subV[0] << endl;
}
}
example.i
// example.i
%module example
%include "std_vector.i"
namespace std {%template(v_int) vector<int>;%template(v_v_int) vector<vector<int>>;};
%{
extern std::vector <std::vector<int>> sum_subVector(std::vector <std::vector<int>> input);
%}
extern std::vector <std::vector<int>> sum_subVector(std::vector <std::vector<int>> input);
run.py
import example
obj=example.sum_subVector(((1,2,3),(3,4,5),(10,10),[20,20]))
for item in obj:
print(item[0])
运行结果为
6
12
20
40
python传入dict,C++返回dict
当然,c++里面不叫dictionary,而叫unordered_map
似乎没有什么特别难/新的知识,按部就班来就行
唯一需要注意的是,即使python接收返回的数据res可以 当成 python dict操作,打印type(res)的时候会发现它仍然是example.i里定义的 map_int_str ,要想真正把它变成python dict,目前想到的唯一方法就是另开一个python dict,然后把key-value一个一个读出来以后一个一个输进去
example.cpp
// example.cpp
#include <Python.h>
#include<iostream>
using namespace std;
unordered_map<int, string> sum_map(unordered_map<int, string> input_map) {
unordered_map<int, string>::iterator iter;
for (iter = input_map.begin(); iter != input_map.end(); iter++) {
if (iter->first < 10) {
string sum_str = "";
for (int i = 0; i < iter->first; i++) {
sum_str += iter->second;
}
iter->second = sum_str;
}
}
return input_map;
}
int main(int argc, char const *argv[]) {
unordered_map<int, string> input;
input[5] = "a";
input[9] = "b";
input[10] = "c";
input[11] = "d";
unordered_map<int, string> res = sum_map(input);
unordered_map<int, string>::iterator iter;
for (iter = res.begin(); iter != res.end(); iter++) {
cout << iter->first << ", " << iter->second << endl;
}
}
example.i
// example.i
%module example
%include "std_string.i"
%include "std_unordered_map.i"
namespace std {%template(map_int_str) unordered_map<int, std::string>;};
%{
extern std::unordered_map<int, std::string> sum_map(std::unordered_map<int, std::string> input_map);
%}
extern std::unordered_map<int, std::string> sum_map(std::unordered_map<int, std::string> input_map);
run.py
import example
res = example.sum_map({5: 'a', 6: 'c', 10: 'd', 11: 'k', 2: 'z'})
print(type(res))
for key in res.keys():
print('%d, %s' % (key, res[key]))
print('-----convert to python dict (what am I doing?)-----')
dict_res = {}
for key in res.keys():
dict_res[key] = res[key]
print(type(dict_res))
for key in dict_res.keys():
print('%d, %s' % (key, dict_res[key]))
运行结果(python):
<class 'example.map_int_str'>
10, d
5, aaaaa
11, k
6, cccccc
2, zz
-----convert to python dict (what am I doing?)-----
<class 'dict'>
10, d
5, aaaaa
11, k
6, cccccc
2, zz
感觉差不多了,几个最紧要的传参问题都能解决,但现在还有一个恶心透顶的问题:
还没找到很好的方法应对swig不支持std::variant的问题
这就导致了不能随便给各种奇奇怪怪结构的数据,也不能混搭着给不同类型的数据,所有数据类型都必须按声明模板进行传递
方案1:继续用swig,想尽办法解决任何遇到的问题
方案2:放弃swig,写纯C++,用一些肮脏的方法接收python的数据(比如监控共享json文件),然后用命令行输出的方法给python传递回去;或者干脆开一个C++ API当作后台,python使用TCP或者unix socket读结果
方案3:放弃swig,转向cython等(看起来)更新一些的技术
又一个挑战:
解决“传入参数为struct类型“的问题:
// example.cpp
#include <Python.h>
#include<iostream>
#include<vector>
struct agent {
std::vector<int> next_node;
std::vector<double> next_dist;
double speed;
};
double sum_double_vector(std::vector<double> input) {
double sum_of_elems = 0;
for (double n: input)
sum_of_elems += n;
return sum_of_elems;
}
std::vector<struct agent> batch_agents(std::vector<struct agent> agents_group) {
for (struct agent agent_i: agents_group) {
if (agent_i.speed >= sum_double_vector(agent_i.next_dist)) {
agent_i.next_node.clear();
agent_i.next_dist.clear();
}
}
return agents_group;
}
python与多线程C代码
最后再补充一个有关多线程的总结:
我有一个10,000,000行的大矩阵想丢到c++里面进行计算(对每行进行单独计算,各行之间相互不干扰),使用常规的方法(vector传入,vector返回)速度似乎和numpy差不太多。为了拉开差距,我决定在c++里面使用2个pthread把矩阵分成2个5,000,000行的矩阵进行并行计算,最后在c++代码中拼出一个10,000,000的矩阵并返回。看起来一切都很美好,但问题就在于:即使是我在c++代码中拼好了矩阵再返回,python接收到的矩阵也有相当大的概率丢失几十行的数据(比如说得到一个9,999,912行的矩阵)。这个问题相当难崩,为了验证这是GIL搞的鬼,我在c++代码里加入了std::lock,把多进程c++变回了实质上是单进程的c++,结果这个矩阵又能完整返回10,000,000行了。但使用了std::lock的c++代码效率还不如单线程c++,所以最后的总结就是python c++ wrapper不太适合数据量大但计算相对不那么复杂的循环/遍历。
比如这个场景,使用c++ wrapper的效率大概率不如python高:10,000,000的矩阵,每行计算sum(row)并返回.
这个场景可能就要使用c++ wrapper了:10,000,000的矩阵,每行的每个浮点数都参与计算一个复杂的幂指数运算,各种判断各种开方各种加减乘除,最后每行返回一个浮点数。