Python C/C++ wrapper学习

This article is categorized as "Garbage" . It should NEVER be appeared in your search engine's results.

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 *

但可以先放在一边


差不多(尝鲜)得差不多了,该系统的学一学这些东西了:

  1. 各种参数传递的情况(比如python传递List过去(特别是那种List of Tuple of List的嵌套结构),C++返回指针或结构体回来,等等)
  2. C++的矩阵计算
  3. 如何把大量数据一次性传递过去进行计算以节省时间(目前认为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的矩阵,每行的每个浮点数都参与计算一个复杂的幂指数运算,各种判断各种开方各种加减乘除,最后每行返回一个浮点数。



 Last Modified in 2024-12-24 

Leave a Comment Anonymous comment is allowed / 允许匿名评论