当项目比较大涉及到多门编程语言时会有这种需求.通常是要求调用C/C++等.
某些语言之间相对来说调用就比较简单,比如Go和C,Rust和C等.但是其他语言相对来说就麻烦了.本文主要涉及Python,JS,Java和C/C+的互相调用,以备不时之需.
TL;DR:Python使用pybind11,JS使用emcc,Java使用JNI.
Python和C或Cpp
Python调用C/Cpp
Ctypes
ctypes 是Python的外部函数库。它提供了与 C语言兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装
写一个c文件1
2
3
4// func.c
int func(int a){
return a*a;
}
编译成动态库1
gcc func.c -fPIC -shared -std=c99 -o func.so
-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。
得到动态库后就能直接调用了,注意在windows上(其实指的是使用MSVC生成dll)需要使用ctypes.WinDLL
1
2
3
4
5
6import ctypes
from ctypes import cdll
if __name__ == "__main__":
f = cdll.LoadLibrary("./func.so")
print(f.func(99))
这种方法缺点是是能调用一些已有的动态库,且不涉及复杂数据结构,只能是c语言.
C/C++扩展Python
使用Python.h
头文件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
static PyObject* list_sum(PyObject *self, PyObject *args)
{
PyObject *pList;
PyObject *pItem;
Py_ssize_t n = 0;
int result = 0;
if(!PyArg_ParseTuple(args, "O!", &PyList_Type, &pList))
{
return NULL;
}
n = PyList_Size(pList);
for (int i=0; i<n; i++) {
pItem = PyList_GetItem(pList, i);
if(!PyInt_Check(pItem)) {
PyErr_SetString(PyExc_TypeError, "list items must be integers.");
return NULL;
}
result += PyInt_AsLong(pItem);
}
return Py_BuildValue("i", result);
}
static PyMethodDef methods[] = {
{ "sum", (PyCFunction)list_sum, METH_VARARGS, "sum method" },
{ NULL, NULL, 0, NULL }
};
static struct PyModuleDef python_api_sum_module = {
PyModuleDef_HEAD_INIT,
"python_api_sum",
"Python interface for the array sum",
-1,
methods
};
PyMODINIT_FUNC PyInit_python_api_sum(void)
{
return PyModule_Create(&python_api_sum_module);
}
1. 使用 C 或 C++ 扩展 Python — Python 3.12.4 文档1
2gcc -Wall -shared -std=c99 -fPIC $(python3-config --includes) $(python3-config --ldflags) test.c -o test$(python3-config --extension-suffix)
在windows上推荐使用msys2工具下载Mingw工具链,
pybind11
这是最简单的方式1
pip install pybind11
1
2
3
4
5
6
7
8
9
10
11
int add(int i, int j) {
return i + j;
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring
m.def("add", &add, "A function that adds two numbers");
}
上面两种方式注意gcc与python版本问题,两者都需要通过gcc/g++访问其下的python的include和lib目录. 我在windows上Mingw的python版本太低,比如--extension-suffix
总是报错,我建议直接在Linux上写.
C/Cpp调用Python
C/C++扩展Python
类似上面的操作1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28#include <Python.h>
int main(int argc, char *argv[]) {
// 初始化python解释器.C/C++中调用Python之前必须先初始化解释器
Py_Initialize();
// 执行一个简单的执行python脚本命令
PyRun_SimpleString("print('hello world')\n");
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append('.')");
PyObject* pModule = PyImport_ImportModule("sum");
if( pModule == NULL ){
cout <<"module not found" << endl;
return 1;
}
// 4、调用函数
PyObject* pFunc = PyObject_GetAttrString(pModule, "say");
if( !pFunc || !PyCallable_Check(pFunc)){
cout <<"not found function add_num" << endl;
return 0;
}
//
PyObject_CallObject(pFunc, NULL);
// 撤销Py_Initialize()和随后使用Python/C API函数进行的所有初始化
Py_Finalize();
return 0;
}
Pybind
同上1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace py = pybind11;
int main(int argc, char *argv[]) {
py::scoped_interpreter guard{};
py::object sum = py::module_::import("sum");
py::object py_list_sum = sum.attr("py_list_sum");
int result = py_list_sum(std::vector<int>{1,2,3,4,5}).cast<int>();
std::cout << "py_list_sum([1,2,3,4,5]) result:" << result << std::endl;
return 0;
}
事实上还有更多方式,不过上面的已经足够了,详细的可以看看其他教程.
一文总结Python和C/C++的交互方式 - 海滨的Blog (hbblog.cn)
第十五章:C语言扩展 — python3-cookbook 2.0.0 文档 (python3-cookbook-personal.readthedocs.io)
这里推荐pybind的方法,相对功能更强,使用也不复杂.
JavaScript和C或Cpp
Js调用C/Cpp
WebAssembly
对于浏览器端,这应该是最通用的方式了.使用Emscripten,将源代码转为assembly格式通过浏览器调用.但是需要浏览器支持.
Compiling a New C/C++ Module to WebAssembly - WebAssembly | MDN (mozilla.org)
按照官网方式下载安装,1
2
3
4
5
int main(int argc, char ** argv) {
printf("Hello World\n");
}1
emcc hello.c -s WASM=1 -o hello.html
-s WASM=1
— 指定我们想要的 wasm 输出形式。最新版emcc默认为1,0表示输出asm.js-o hello.html
— 指定这个选项将会生成 HTML 页面来运行我们的代码,并且会生成 wasm 模块,以及编译和实例化 wasm 模块所需要的“胶水”js 代码,这样我们就可以直接在 web 环境中使用了。
这个时候在你的源码文件夹应该有下列文件:
hello.wasm
二进制的 wasm 模块代码hello.js
一个包含了用来在原生 C 函数和 JavaScript/wasm 之间转换的胶水代码的 JavaScript 文件hello.html
一个用来加载,编译,实例化你的 wasm 代码并且将它输出在浏览器显示上的一个 HTML 文件
使用一个支持 WebAssembly 的浏览器,加载生成的 hello.html
。自从 Firefox 版本 52、Chrome 版本 57 和 Opera 版本 44 开始,已经默认启用了 WebAssembly(注意不是通过文件方式打开)
上面是在html加载后会调用main函数中的代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main(int argc, char ** argv) {
printf("Hello World\n");
}
extern "C" {
int EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
printf("我的函数已被调用\n");
}
}1
emcc -o hello3.html hello3.c -O3 -s WASM=1 -s "EXPORTED_RUNTIME_METHODS=['ccall']" --shell-file html_template/shell_minimal.html
如果要导入函数,通过设置EXPORTED_RUNTIME_METHODS导出ccall,而ccall会在 JS 代码之中调用 C 函数1
2
3
4
5
6
7
8
9
10//<button class="mybutton">运行我的函数</button> //html
document.querySelector(".mybutton").addEventListener("click", function () {
alert("检查控制台");
var result = Module.ccall(
"myFunction", // name of C function
null, // return type
null, // argument types
null,
); // arguments
});
asm.js 和 Emscripten 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)
emcc既支持asm.js也支持WASM,两者都能实现类似的效果,不过目前还是WASM风头正劲
c++ addons
C++ addons | Node.js v20.15.0 Documentation (nodejs.org)
使用node.h
头文件并下载node-gyp编译得到动态库,再通过node调用.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// hello.cc
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(
isolate, "world").ToLocalChecked());
}
void Initialize(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
} // namespace demo
这个也要注意使用的编译器,我在windows上默认使用visual studio,需要后缀.cpp.
创建binding.gyp,然后使用node-gyp1
2
3
4
5
6
7
8
9
10{
'targets': [
{
'target_name': 'hello',
'sources': [
'src/hello.cc',
]
}
]
}1
2node-gyp configure
node-gyp build
编译后得到.node文件使用js调用即可1
2
3
4
5try {
return require('./build/Release/addon.node');
} catch (err) {
return require('./build/Debug/addon.node');
}
Native abstractions for Node.js
Node-API 是用于构建native addons的 API。它独立于底层 JavaScript 运行时(如 V8),并作为 Node.js 自身的一部分进行维护。该 API 在不同版本的 Node.js 中具有稳定的应用二进制接口 (ABI)。其目的是使附加组件不受底层 JavaScript 引擎变化的影响,并允许为某一版本编译的模块无需重新编译即可在以后版本的 Node.js 上运行。addons使用node-gyp 等构建/打包。
C++ addons | Node.js v20.15.0 Documentation (nodejs.org)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// hello.cc using Node-API
namespace demo {
napi_value Method(napi_env env, napi_callback_info args) {
napi_value greeting;
napi_status status;
status = napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &greeting);
if (status != napi_ok) return nullptr;
return greeting;
}
napi_value init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, Method, nullptr, &fn);
if (status != napi_ok) return nullptr;
status = napi_set_named_property(env, exports, "hello", fn);
if (status != napi_ok) return nullptr;
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
} // namespace demo
好处是兼容性更高,api看起来也更易懂.
C/Cpp调用Js
C addons
同上,可以考虑使用回调等方法在c中调用js.
asm.js 和 Emscripten 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)
如果是node那就按照官方文档使用addons(事实上也可以使用Emscripte转为asm.js进行调用),如果是浏览器,那推荐使用WASM,除能转换c/c++之外还有Rust等,在前端也是有前景的技术之一.Emscripten
通过asm.js调用c++代码,方法类似,目前emcc的WASM默认为1也就是默认生成WASM,但使用时通过下面命令得到asm.js1
emcc index.cpp -s "EXPORTED_FUNCTIONS=[‘_main’,'_myFunction']" -s WASM=0 -s EXPORTED_RUNTIME_METHODS="['ccall']" -o index.js
在js文件中调用1
2
3
4
5
6
7
8let module = require("./output.js")
let resulst = module.onRuntimeInitialized(()=>{
module.ccall('myFunction',{
null, // return type
null, // argument type
null, // arguments
})
})
asm.js 的技术能将 C / C++ 转成 JS 引擎可以运行的代码。那么它与 WASM有何区别呢?
回答是,两者的功能基本一致,就是转出来的代码不一样:asm.js 是文本,WebAssembly 是二进制字节码,因此运行速度更快、体积更小。从长远来看,WebAssembly 的前景更光明。
但是,这并不意味着 asm.js 肯定会被淘汰,因为它有两个优点:首先,它是文本,人类可读,比较直观;其次,所有浏览器都支持 asm.js,不会有兼容性问题。
Java和C或Cpp
Java调用C/Cpp有多种方式,这里只介绍一种.
JNI,通过native声明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package org.example;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
public native void sayHello();
static {
// System.load("./sayHello.dll");
System.loadLibrary("sayHello");
}
public static void main(String[] args) {
System.out.println("Hi");
System.out.println(System.getProperty("java.library.path"));
Main m = new Main();
m.sayHello();
}
}
使用javac -h ./ Main.java
转为头文件,内容如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/* DO NOT EDIT THIS FILE - it is machine generated */
/* Header for class org_example_Main */
extern "C" {
/*
* Class: org_example_Main
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_org_example_Main_sayHello
(JNIEnv *, jobject);
}
然后写一个cpp去实现方法1
2
3
4
5
JNIEXPORT void JNICALL Java_org_example_Main_sayHello(JNIEnv *, jobject) {
std::cout << "Hello im from cpp" << std::endl;
}
然后生成dll文件,注意头文件要有jdk中的头文件1
g++ -Wall -shared -fPIC -IC:/Users/proanimer/.jdks/openjdk-22.0.1/include -IC:/Users/proanimer/.jdks/openjdk-22.0.1/include/win32 sayHello.cpp -o sayHello.dll
得到的dll文件就能被`System.loadLibrary
加载了,但注意dll文件放的位置,会去环境变量中的PATH去找,如果直接放在同一目录下没有额外设置是没有导入的.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package org.example;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
public native void sayHello();
static {
System.loadLibrary("sayHello");
}
public static void main(String[] args) {
System.out.println("Hi");
System.out.println(System.getProperty("java.library.path"));
Main m = new Main();
m.sayHello();
}
}
也可以通过class打包为jarr包,然后直接调用jar包(jar包需要调用dll)即可.
java -jar <jarfile>
: 运行指定的 Java 归档文件(JAR 文件)。java -cp <classpath> <main-class>
: 指定类路径并运行指定的主类。java -D<property>=<value>
: 设置 Java 系统属性。java -verbose
: 开启详细输出模式。java -version
: 显示 Java 版本信息。java -help
: 显示 Java 命令行帮助。
其他方法可以看看JNA —— Java调用C/C++动态库_jna调用c++类-CSDN博客