不同编程语言之间的互操作:Py,Js,Java调用C/C++

个人博客:Sekyoro的博客小屋
个人网站:Proanimer的个人网站
当项目比较大涉及到多门编程语言时会有这种需求.通常是要求调用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文件

// func.c
int func(int a){
return a*a;
}

编译成动态库

gcc func.c -fPIC -shared -std=c99 -o func.so

-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code),则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

得到动态库后就能直接调用了,注意在windows上(其实指的是使用MSVC生成dll)需要使用ctypes.WinDLL

import ctypes
from ctypes import cdll

if __name__ == "__main__":
    f = cdll.LoadLibrary("./func.so")
    print(f.func(99))

这种方法缺点是是能调用一些已有的动态库,且不涉及复杂数据结构,只能是c语言.

C/C++扩展Python

使用Python.h头文件

#include <Python.h>

#if PY_MAJOR_VERSION >= 3
#define PyInt_Check PyLong_Check
#define PyInt_AsLong PyLong_AsLong
#endif

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 文档

gcc -Wall -shared  -std=c99 -fPIC $(python3-config --includes) $(python3-config --ldflags) test.c -o test$(python3-config --extension-suffix)

在windows上推荐使用msys2工具下载Mingw工具链,

pybind11

这是最简单的方式

pip install pybind11
#include <pybind11/pybind11.h>

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

类似上面的操作

#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

同上

#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/embed.h>
#include <vector>
#include <iostream>

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)

按照官网方式下载安装,

#include <stdio.h>

int main(int argc, char ** argv) {
  printf("Hello World\n");
}
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函数中的代码

#include <stdio.h>
#include <emscripten/emscripten.h>

int main(int argc, char ** argv) {
    printf("Hello World\n");
}

#ifdef __cplusplus
extern "C" {
#endif

int EMSCRIPTEN_KEEPALIVE myFunction(int argc, char ** argv) {
  printf("我的函数已被调用\n");
}

#ifdef __cplusplus
}
#endif
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 函数

//<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调用.

// hello.cc
#include <node.h>

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-gyp

{
    'targets': [
        {
            'target_name': 'hello',
            'sources': [ 
                'src/hello.cc',
            ]
        }
    ]
}
node-gyp configure
node-gyp build

编译后得到.node文件使用js调用即可

try {
  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)

// hello.cc using Node-API
#include <node_api.h>

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.js

emcc index.cpp -s "EXPORTED_FUNCTIONS=[‘_main’,'_myFunction']" -s WASM=0 -s EXPORTED_RUNTIME_METHODS="['ccall']" -o index.js

在js文件中调用

let 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声明

package 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转为头文件,内容如下

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_example_Main */

#ifndef _Included_org_example_Main
#define _Included_org_example_Main
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     org_example_Main
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_org_example_Main_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

然后写一个cpp去实现方法

#include <iostream>
#include "org_example_Main.h"
JNIEXPORT void JNICALL Java_org_example_Main_sayHello(JNIEnv *, jobject) {
  std::cout << "Hello im from cpp" << std::endl;
}

然后生成dll文件,注意头文件要有jdk中的头文件

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去找,如果直接放在同一目录下没有额外设置是没有导入的.

package 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)即可.

  1. java -jar <jarfile>: 运行指定的 Java 归档文件(JAR 文件)。
  2. java -cp <classpath> <main-class>: 指定类路径并运行指定的主类。
  3. java -D<property>=<value>: 设置 Java 系统属性。
  4. java -verbose: 开启详细输出模式。
  5. java -version: 显示 Java 版本信息。
  6. java -help: 显示 Java 命令行帮助。

其他方法可以看看JNA —— Java调用C/C++动态库_jna调用c++类-CSDN博客

参考资料

  1. gcc生成静态库与动态库(附带使用方法)_gcc 生成静态库-CSDN博客
  2. C++静态库与动态库 | 菜鸟教程 (runoob.com)

如有疑问,欢迎各位交流!

服务器配置
宝塔:宝塔服务器面板,一键全能部署及管理
云服务器:阿里云服务器
Vultr服务器
GPU服务器:Vast.ai

  • 8
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

procoder338

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值