国外Rust程序员分享:Rust与C++的完美结合

163 阅读7分钟

图片创作不易,方便的话点点关注,谢谢

文章结尾有最新热度的文章,感兴趣的可以去看看。

本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身

文章有点长(2400字阅读时长:5分),期望您能坚持看完,并有所收获。

导读

Interop到c++是通过经典的沙漏方法完成的。我们将Rust库连接到C,并创建可以安全地使用C接口的c++类型。这与Python等其他语言的interop工作方式相同。

图片

我们需要一种方法让C++拥有那些Rust对象来调用以Rust对象为参数的Rust函数。我们必须创建Rust对象并将指针泄露给C++代码以实现这一点。我们还在Rust中包含了可以销毁这些对象的函数,给定一个指向它们的指针。我们可以使用一个C++类,其成员是指向Rust对象的不透明指针,使用其析构函数方法来释放它们。这是必要的一个重要原因是分配器和释放器成对出现。使用C++释放器销毁Rust对象或反之是无效的。

图片

在我们必须从Rust库类型创建C++库类型的情况下,例如从nalgebra::geometry::Isometry3创建Eigen::Isometry3d,我们必须复制底层数据而不是共享内存。这是因为,在C++中,我们不能扩展库类型以使用不同的释放器来处理底层内存的销毁。

在Rust同质变换类型nalgebra::geometry::Isometry3的特定情况下,底层数据是由单个16个双精度浮点数数组表示的4x4矩阵。固定大小的数组是我们可以在FFI边界上传递的东西。我们将利用这一点来避免额外的复制或分配。

与C++构建系统集成的担忧

由于我工作中的C++代码使用CMake,我将链接到一个示例,展示如何使这个C++项目可以被其他CMake项目使用。

我将把我的项目分成两个Rust crate(包)以便于代码布局。

  • • robot_joint - 我想从C++使用的Rust库(你所拥有的)

  • • robot_joint-cpp - C++互操作层(你想编写的)

自定义不透明类型

给定这个Rust结构和工厂函数,我们必须创建一个C接口。

pub structJoint{
    name:String,
    parent_link_to_joint_origin:Isometry3<f64>,
}

implJoint{
pubfnnew()->Self;
}

在robot_joint-cpp中,我创建了一个包含这些细节的lib.rs。

use robot_joint::Joint;

#[no_mangle]
extern"C"fnrobot_joint_new()->*mutJoint{
Box::into_raw(Box::new(Joint::new()))
}

#[no_mangle]
extern"C"fnrobot_joint_free(joint:*mutJoint){
unsafe{
drop(Box::from_raw(joint));
}
}

每个函数都需要#[no_mangle]属性来关闭Rust名称修饰,extern "C"来给函数C调用约定。Box::into_raw(Box::new()是一种在堆上创建Rust对象并泄露指针的技术。最后,drop(Box::from_raw)是一种将指针转换回Box类型并销毁它的方法。

接下来,我们创建一个C++头文件robot_joint.hpp。

#pragma once

#include <memory>

namespace robot_joint::rust {

/// 对Rust对象的指针使用的不透明类型的前置声明。
structJoint;

}// namespace robot_joint::rust

extern"C"{
extern void robot_joint_free(robot_joint::rust::Joint*);
}

/// 从函数模板参数创建自定义删除器。
template<auto fn>
structdeleter_from_fn{
template<typename T>
  constexpr void operator()(T* arg) const {
fn(arg);
}
};

namespace robot_joint {

/// 对robot_joint对象的不可移动句柄(生活在Rust一侧)。
classJoint{
public:
Joint()noexcept;
~Joint()noexcept=default;

Joint(Joint&& other)noexcept=default;
Joint&operator=(Joint&& other)noexcept=default;

private:
  std::unique_ptr<rust::Joint, deleter_from_fn<robot_joint_free>> robot_joint_;
};

} // namespace robot_joint

C++提供了一种特殊的指针类型,如果我们将删除器指定为模板参数,它将为我们处理Rust对象的清理。为了设置对Rust析构函数的调用,我们必须在头文件中有一个extern定义。由于unique_ptr是不可移动的,复制构造和赋值被禁用。这个C++类现在尽可能安全。

#include "robot_joint.hpp"

extern"C"{
extern robot_joint::rust::Joint* robot_joint_new();
}

namespace robot_joint {

Joint::Joint()noexcept
: joint_{robot_joint_new()}{
}

} // namespace robot_joint

在这里,我们创建了我们的C++接口的源文件。我们再次使用extern "C"来使我们的C++代码能够调用从Rust创建joint对象的C函数。

最后,最具挑战性的部分是使其与CMake项目兼容。我写了一篇关于该主题的后续博客文章。

一等库类型

记住,我说我采取手动方法,因为我想要一个在C++一侧具有Eigen类型的接口。以下是如何实现这一点的简单示例。假设我们有这个Rust函数在我们的Joint类型上。

impl Joint {
    pub fn calculate_transform(&self, variables: &[f64]) -> Isometry3<f64>;
}

我们想要创建一个这样的C++接口。

class Joint {
  public:
    Eigen::Isometry3d calculate_transform(const Eigen::VectorXd& variables);
};

首先,我们必须为这个函数创建Rust FFI接口。

use std::ffi::{c_double, c_uint};

#[repr(C)]
structMat4d{
    data:[c_double;16],
}

#[no_mangle]
extern"C"fnrobot_joint_calculate_transform(
    joint:*constJoint,
    variables:*const c_double,
    size: c_uint,
)->Mat4d{
unsafe{
letjoint= joint.as_ref().expect("Invalid pointer to Joint");
letvariables= std::slice::from_raw_parts(variables, size asusize);
lettransform= joint.calculate_transform(variables);
Mat4d{
            data: transform.to_matrix().as_slice().try_into().unwrap(),
}
}
}

Rust标准库中的ffi模块提供了我们需要的C类型参数。在调用rust calculate_transform之前,我们必须首先从参数构建Rust类型。

有趣的是,我们利用了一个未记录的事实,即在ffi中可以使用瘦指针。一个大小已知的切片是一个在运行时不存储大小的瘦指针。我们可以通过将其放入一个结构体并设置内存表示为C来按值返回一个大小已知的切片。

然后,我们可以编写一个调用C函数的C++函数。

struct Mat4d{
double data[16];
};

extern"C"{
externstructMat4drobot_joint_calculate_transform(
const robot_joint::rust::Joint*,constdouble*,unsignedint);
}

namespace robot_joint {
Eigen::Isometry3d Joint::calculate_transform(const Eigen::VectorXd& variables)
{
constauto rust_isometry =robot_joint_calculate_transform(
    joint_, variables.data(), variables.size());
Eigen::Isometry3d transform;
  transform.matrix()=Eigen::Map<Eigen::Matrix4d>(rust_isometry.data);
return transform;
}
}  // namespace robot_joint

从robot_joint_calculate_transform返回的Rust Mat4d类型包含一个十六个双精度浮点数的固定大小数组。我们可以使用这个数组类型转换一个4x4的Eigen矩阵,并将其赋值给Isometry3d,然后返回它。

结论

构建一个创造优秀的C++和Rust接口的桥梁比许多人想象的要简单。你可能会在说服你的C++同事让你用Rust编写代码方面遇到更多麻烦,而不是进行互操作。图片

没有测试的代码应该被认为是破损的。为了信任所有这些不安全的C++和Rust代码,我们应该编写测试来锻炼所有代码路径,并使用sanitizers运行它们。在将来的帖子中,我将向你展示如何使用优秀的C++ Catch2库来测试你的C++绑定,并使用地址和未定义行为sanitizers。我写了一篇关于CMake集成的后续文章来解释如何做到这一点。

最新热门文章推荐:

白宫关注下,C++的内存安全未来走向何方?

国外Python程序员分享:如何用Python构建一个多代理AI应用

本地部署六款大模型:保护隐私、节省成本,特定环境首选

国外CUDA程序员分享:2024年GPU编程CUDA C++(从环境安装到进阶技巧)

我卸载了VSCode,我的生产力大幅提升

国外Python程序员分享:2024年NumPy高性能计算库(高级技巧)

外国人眼中的程明明:从“电脑小白”到CV领域领军者

外国人眼中的周志华:人工智能奖获得者、人工智能学院院长

国外C++程序员分享:C++多线程实战掌握图像处理高级技巧

外国人眼中的卢湖川:从大连理工到全球舞台,他的科研成果震撼世界!

外国人眼中的张祥雨:交大90后男神博士,3年看1800篇论文,还入选福布斯精英榜

参考文献:《图片来源网络》《Rust/C++ Interop Part 1 - Just the Basics》

本文使用 文章同步助手 同步