当前位置: 首页 > news >正文

【CXX】2 CXX blobstore客户端说明

本示例演示了一个调用blobstore服务的C++客户端的Rust应用程序。事实上,我们会看到两个方向的调用:Rust到C++以及C++到Rust。对于您自己的用例,您可能只需要其中一个方向。
示例中涉及的所有代码都显示在此页面上,但它也以可运行的形式提供在demo目录中https://github.com/dtolnay/cxx.要直接尝试,请从该目录运行cargo run。
共享结构、不透明类型和函数已经在上一篇文章中叙述,不清楚的可以先去看一下。

一、创建项目

我们在命令行中创建一个空白的Cargo项目:
cargo new cxx-demo
编辑Cargo.toml文件,添加对cxx的依赖:

[dependencies]
cxx = "1.0"

二、定义语言边界

CXX依赖于对每种语言向另一种语言公开的函数签名的描述。您可以在Rust模块中使用extern块提供此描述,该模块用#[cxx::bridge]属性宏注释。
我们在项目的main.rs文件的顶部添加该内容:

#[cxx::bridge]
mod ffi {
}

该内容将是FFI边界双方需要达成一致的所有内容。

三、从Rust调用C++函数

让我们获取一个C++blobstore客户端的实例,一个在C++中定义的类blobstore client。
我们将把BlobstreClient视为CXX分类中的不透明类型,这样Rust就不需要对其实现做出任何假设,甚至不需要对它的大小或对齐方式做出任何假设。一般来说,C++类型可能有一个与Rust的move语义不兼容的move构造函数,或者可能包含Rust的借用系统无法建模的内部引用。尽管有其他选择,但在FFI边界上不关心任何此类事情的最简单方法是将其视为不透明,不需要了解类型。
不透明类型只能在间接后面操作,如引用&、Rust Box或UniquePtr(std::unique_ptr的Rust绑定)。我们将添加一个函数,通过该函数,C++可以向Rust返回std::unique_ptr。

// src/main.rs

#[cxx::bridge]
mod ffi {
    unsafe extern "C++" {
        include!("cxx-demo/include/blobstore.h");

        type BlobstoreClient;

        fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
    }
}

fn main() {
    let client = ffi::new_blobstore_client();
}

即使CXX自动执行静态断言,确保签名与C++中声明的完全匹配,我们仍然需要确保键入的签名是准确的。比如new_blobstore_client函数如果会发生意外(如内存错误)必须用unsafe标记。这次是在一个安全的extern“C++”块中,因为程序员不再需要对签名进行任何安全声明。

四、添加C++代码

在CXX与Cargo的集成中,默认情况下,所有#include路径都以单元包(crate)名称开头。这就是为什么我们看到 include!(“cxx-demowj/include/blobstore.h”) ——我们将把C++头文件放在Rust单元包内的相对路径include/blostore.h处。如果根据Cargo.toml中的name字段,你的crate的名称不是cxx-demo,那么在本教程中,你需要在所有地方使用这个名称来代替cxx-demo。

// include/blobstore.h

#pragma once
#include <memory>

class BlobstoreClient {
public:
  BlobstoreClient();
};

std::unique_ptr<BlobstoreClient> new_blobstore_client();

// src/blobstore.cc

#include "cxx-demo/include/blobstore.h"

BlobstoreClient::BlobstoreClient() {}

std::unique_ptr<BlobstoreClient> new_blobstore_client() {
  return std::unique_ptr<BlobstoreClient>(new BlobstoreClient());
}

使用std::make_unique也可以,只要你将std(“c++14”)传递给c++编译器,如稍后所述。
include/和src/中的位置并不重要;只要在整个项目中使用正确的路径,就可以将C++代码放置在单元包中的任何其他位置。
请注意,CXX不会查看这些文件中的任何一个。你可以自由地在这里放任意的C++代码, #include你自主的库等等。CXX库所做的就是针对您在头文件中提供的内容发出静态断言。

五、用Cargo编译C++代码

Cargo有一个适合编译非Rust代码的构建脚本功能。
我们需要在Cargo.toml中引入对CXX的C++代码生成器的新的构建时依赖:

# Cargo.toml

[dependencies]
cxx = "1.0"

[build-dependencies]
cxx-build = "1.0"

然后在Cargo.toml旁边添加一个build.rs构建脚本,以运行cxx构建代码生成器和C++编译器。相关参数是包含cxx::bridge语言边界定义的Rust源文件的路径,以及在Rust crate构建过程中要编译的任何其他C++源文件的道路。

// build.rs

fn main() {
    cxx_build::bridge("src/main.rs")
        .file("src/blobstore.cc")
        .compile("cxx-demo");

    println!("cargo:rerun-if-changed=src/main.rs");
    println!("cargo:rerun-if-changed=src/blobstore.cc");
    println!("cargo:rerun-if-changed=include/blobstore.h");
}

他的build.rs也是您设置C++编译器标志的地方,例如,如果您想从C++14访问std::make_unique。

 cxx_build::bridge("src/main.rs")
        .file("src/blobstore.cc")
        .std("c++14")
        .compile("cxx-demo");

尽管还没有做任何有用的事情,该项目现在应该能够成功构建和运行。命令行输入命令如下:

<cxx-demo路径提示符>  cargo run
  Compiling cxx-demo v0.1.0
  Finished dev [unoptimized + debuginfo] target(s) in 0.34s
  Running `target/debug/cxx-demo`

<cxx-demo路径提示符>

六、从C++调用Rust函数

我们的C++blobstore支持不连续缓冲区上传的put操作。例如,我们可能正在上传一个循环缓冲区的快照,该缓冲区往往由2个部分组成,或者由于其他原因(如绳索数据结构)而分散在内存中的文件片段。
我们将通过在连续的借用块上传递迭代器来表达这一点。这与广泛使用的字节箱的Buf特性的API非常相似。在put过程中,我们将让C++回调到Rust中,以获取上传的连续块(所有块都没有在语言边界上进行复制或分配)。实际上,C++客户端可能包含一些复杂的块批处理或并行上传,所有这些都与之相关。

// src/main.rs

#[cxx::bridge]
mod ffi {
    extern "Rust" {
        type MultiBuf;

        fn next_chunk(buf: &mut MultiBuf) -> &[u8];
    }

    unsafe extern "C++" {
        include!("cxx-demo/include/blobstore.h");

        type BlobstoreClient;

        fn new_blobstore_client() -> UniquePtr<BlobstoreClient>;
        fn put(&self, parts: &mut MultiBuf) -> u64;
    }
}

任何具有self参数的签名(等同C++的this)都被认为是一个方法/非静态成员函数。如果周围的extern块中只有一个类型,则它将是该类型的方法。如果有多个类型,您可以通过在参数列表中编写self:&BlostreClient来区分方法属于哪一个。
像往常一样,现在我们需要提供extern“Rust”块声明的所有内容的Rust定义,以及extern“C++”块宣布的新签名的C++定义。

// src/main.rs

// An iterator over contiguous chunks of a discontiguous file object. Toy
// implementation uses a Vec<Vec<u8>> but in reality this might be iterating
// over some more complex Rust data structure like a rope, or maybe loading
// chunks lazily from somewhere.
pub struct MultiBuf {
    chunks: Vec<Vec<u8>>,
    pos: usize,
}

pub fn next_chunk(buf: &mut MultiBuf) -> &[u8] {
    let next = buf.chunks.get(buf.pos);
    buf.pos += 1;
    next.map_or(&[], Vec::as_slice)
}
// include/blobstore.h

struct MultiBuf;

class BlobstoreClient {
public:
  BlobstoreClient();
  uint64_t put(MultiBuf &buf) const;
};

在blobstre.cc中,我们可以调用Rust next_chunk函数,该函数通过CXX代码生成器生成的头部文件main.rs.h暴露给C++。在CXX的Cargo集成中,这个生成的头文件有一个包含crate名称、crate中Rust源文件的相对路径和.rs.h扩展名的路径。

// src/blobstore.cc

#include "cxx-demo/include/blobstore.h"
#include "cxx-demo/src/main.rs.h"
#include <functional>
#include <string>

// Upload a new blob and return a blobid that serves as a handle to the blob.
uint64_t BlobstoreClient::put(MultiBuf &buf) const {
  // Traverse the caller's chunk iterator.
  std::string contents;
  while (true) {
    auto chunk = next_chunk(buf);
    if (chunk.size() == 0) {
      break;
    }
    contents.append(reinterpret_cast<const char *>(chunk.data()), chunk.size());
  }

  // Pretend we did something useful to persist the data.
  auto blobid = std::hash<std::string>{}(contents);
  return blobid;
}

现在可以使用了

// src/main.rs

fn main() {
    let client = ffi::new_blobstore_client();

    // Upload a blob.
    let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
    let mut buf = MultiBuf { chunks, pos: 0 };
    let blobid = client.put(&mut buf);
    println!("blobid = {}", blobid);
}

运行信息如下:

cxx-demo$  cargo run
  Compiling cxx-demo v0.1.0
  Finished dev [unoptimized + debuginfo] target(s) in 0.41s
  Running `target/debug/cxx-demo`

blobid = 9851996977040795552

七、插曲:产生了什么?

对于好奇的人来说,很容易了解CXX为使这些函数调用工作所做的幕后工作。在CXX的正常使用过程中,您不需要这样做,但就本教程而言,这可能具有教育意义。
CXX包含两个代码生成器:一个Rust生成器(即CXX::bridge属性过程宏)和一个C++生成器。
Rust生成的代码
通过cargo-expand可以最容易地查看程序宏的输出。然后运行cargo expand ::ffi宏展开mod ffi模块。

cxx-demo$  cargo install cargo-expand 
cxx-demo$  cargo expand ::ffi

您将看到一些非常令人不快的代码,涉及#[repr(C)]、#[repr©], #[link_name] 和 #[export_name].。

八、C++生成的代码

为了调试方便,cxx_build将所有生成的C++代码链接到Cargo在target/cxxbridge/下的目标目录中。

cxx-demo$  exa -T target/cxxbridge/
target/cxxbridge
├── cxx-demo
│  └── src
│     ├── main.rs.cc -> ../../../debug/build/cxx-demo-11c6f678ce5c3437/out/cxxbridge/sources/cxx-demo/src/main.rs.cc
│     └── main.rs.h -> ../../../debug/build/cxx-demo-11c6f678ce5c3437/out/cxxbridge/include/cxx-demo/src/main.rs.h
└── rust
   └── cxx.h -> ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cxx-1.0.0/include/cxx.h

在这些文件中,您将看到语言边界中存在的任何CXX Rust类型的声明或模板(如Rust::Slicefor&[T])以及与extern函数对应的extern“C”签名。
如果CXX C++代码生成器更适合您的工作流程,它也可以作为一个独立的可执行文件提供,将生成的代码输出到stdout。

cxx-demo$  cargo install cxxbridge-cmd
cxx-demo$  cxxbridge src/main.rs

九、共享数据结构

到目前为止,上述两个方向的调用只使用了不透明类型,而不是共享结构。
共享结构是数据结构,其完整定义对两种语言都是可见的,从而可以通过值跨语言传递它们。共享结构转换为C++聚合初始化兼容结构,与Rust结构的布局完全匹配。
作为此演示的最后一步,我们将使用共享结构体BlobMetadata在Rust应用程序和C++blobstore客户端之间传递有关blob的元数据。

// src/main.rs

#[cxx::bridge]
mod ffi {
    struct BlobMetadata {
        size: usize,
        tags: Vec<String>,
    }

    extern "Rust" {
        // ...
    }

    unsafe extern "C++" {
        // ...
        fn tag(&self, blobid: u64, tag: &str);
        fn metadata(&self, blobid: u64) -> BlobMetadata;
    }
}

fn main() {
    let client = ffi::new_blobstore_client();

    // Upload a blob.
    let chunks = vec![b"fearless".to_vec(), b"concurrency".to_vec()];
    let mut buf = MultiBuf { chunks, pos: 0 };
    let blobid = client.put(&mut buf);
    println!("blobid = {}", blobid);

    // Add a tag.
    client.tag(blobid, "rust");

    // Read back the tags.
    let metadata = client.metadata(blobid);
    println!("tags = {:?}", metadata.tags);
}
// include/blobstore.h

#pragma once
#include "rust/cxx.h"

struct MultiBuf;
struct BlobMetadata;

class BlobstoreClient {
public:
  BlobstoreClient();
  uint64_t put(MultiBuf &buf) const;
  void tag(uint64_t blobid, rust::Str tag) const;
  BlobMetadata metadata(uint64_t blobid) const;

private:
  class impl;
  std::shared_ptr<impl> impl;
};

// src/blobstore.cc

#include "cxx-demo/include/blobstore.h"
#include "cxx-demo/src/main.rs.h"
#include <algorithm>
#include <functional>
#include <set>
#include <string>
#include <unordered_map>

// Toy implementation of an in-memory blobstore.
//
// In reality the implementation of BlobstoreClient could be a large
// complex C++ library.
class BlobstoreClient::impl {
  friend BlobstoreClient;
  using Blob = struct {
    std::string data;
    std::set<std::string> tags;
  };
  std::unordered_map<uint64_t, Blob> blobs;
};

BlobstoreClient::BlobstoreClient() : impl(new class BlobstoreClient::impl) {}

// Add tag to an existing blob.
void BlobstoreClient::tag(uint64_t blobid, rust::Str tag) const {
  impl->blobs[blobid].tags.emplace(tag);
}

// Retrieve metadata about a blob.
BlobMetadata BlobstoreClient::metadata(uint64_t blobid) const {
  BlobMetadata metadata{};
  auto blob = impl->blobs.find(blobid);
  if (blob != impl->blobs.end()) {
    metadata.size = blob->second.data.size();
    std::for_each(blob->second.tags.cbegin(), blob->second.tags.cend(),
                  [&](auto &t) { metadata.tags.emplace_back(t); });
  }
  return metadata;
}

运行命令:

cxx-demo$  cargo run
  Running `target/debug/cxx-demo`

blobid = 9851996977040795552
tags = ["rust"]

现在您已经看到了本教程中涉及的所有代码。它可以在演示目录中以可运行的形式一起使用https://github.com/dtolnay/cxx.您可以直接运行它,而无需从该目录运行cargo run来完成上述步骤。

十、结束语

CXX的主要贡献是它为你提供了Rust与C++的互操作性,在这种互操作性中,你编写的所有Rust端代码看起来都像是在写普通的Rust,而C++端看起来也像是在编写普通的C++。
在文中,您已经看到,所涉及的代码都不像C,也不像通常危险的“FFI胶水”,容易发生泄漏或内存安全缺陷。
由不透明类型、共享类型和关键标准库类型绑定组成的表达系统使API能够在语言边界上进行设计,从而获取接口的正确所有权和借用契约。
CXX发挥了Rust类型系统和C++类型系统的优势以及程序员的直觉。一个在没有Rust背景的C++端或没有C++背景的Rust端工作的人,将能够运用他们对语言开发的所有常见直觉和最佳实践来维护正确的FFI。

相关文章:

  • 链表 —— 常用技巧与操作总结详解
  • Android studio常量表达式的错误
  • 分布式服务框架 如何设计一个更合理的协议
  • Python分享20个Excel自动化脚本
  • 作业。。。。。
  • 在vivado中对数据进行延时,时序对齐问题上的理清
  • 蓝桥杯 Java B 组之枚举算法(暴力破解)
  • AI赋能创业:ScriptEcho如何助力快速搭建前端应用
  • joint_info.npz 找不到
  • mapbox 从入门到精通 - 目录
  • 基于python sanic框架,使用Nacos进行微服务管理
  • 苹果公司宣布正式开源 Xcode 引擎 Swift Build145
  • MySQL的innoDB引擎
  • Mac之JDK安装
  • Pyqt6 中 QMediaPlayer 音视频播放
  • DeepSeek免费部署到WPS或Office
  • 鸿蒙面试题
  • Kotlin 扩展函数与内联函数
  • python视频爬虫
  • 2025 年 2 月 TIOBE 指数
  • 美国务卿鲁比奥抵达会场,将参加俄乌会谈
  • 巴菲特最新调仓:一季度大幅抛售银行股,再现保密仓位
  • 讲座预告|以危机为视角解读全球治理
  • 艺术稀缺性和价值坚守如何构筑品牌差异化壁垒?从“心邸”看CINDY CHAO的破局之道
  • 辽宁盘山县一乡镇幼儿园四名老师被指多次殴打一女童,均被行拘
  • 南方降水频繁暴雨连连,北方高温再起或现40°C酷热天气