从Rust到远方:C星系

2024-06-23 01:08
文章标签 rust 远方 星系

本文主要是介绍从Rust到远方:C星系,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

来源:https://mnt.io/2018/09/11/from-rust-to-beyond-the-c-galaxy/

这篇博客文章是这一系列解释如何将Rust发射到地球以外的许多星系的文章的一部分:

  • 前奏,

  • WebAssembly 星系

  • ASM.js星系

  • C星系(当前这一集)

  • PHP星系,以及

  • NodeJS 星系

今天将要探索的是C语言星系。这篇文章会解释什么是C语言(比较简要),理论上怎样编译Rust供C使用,以及如何在实际使用从Rust和C两方面来实现我们的Rust解析器。我们还将看到如何测试这样的绑定。

什么是C语言,为什么有C?

C应该是在全球范围内被应用和被知道的最为广泛的一种编程语言。Wikipedia的引用:

C[...] 是一种通用的命令式计算机编程语言,支持结构化编程、词法变量作用域和递归,而静态类型系统可以防止许多意外操作。通过设计,C提供了有效地映射到典型机器指令的构造,因此它在以前用汇编语言编码的应用程序中得到了持久的使用,包括操作系统,以及从超级计算机到嵌入式系统的各种计算机应用软件。

640?wx_fmt=jpeg

Dennis Ritchie, C语言的发明者.

C语言对编程语言世界的影响可能是史无前例的。从操作系统开始以及之上的几乎所有的东西都是用C语言写的。今天,它是世界上为数不多的通用标准,链接任何机器上的任何系统上的任何程序。换句话说,与C语言兼容为所有事情打开了一扇大门。您的程序将能够直接与任何程序轻松对话。

因为像PHP或Python这样的语言都是用C语言编写的,在我们特定的Gutenberg解析器用例中,这意味着解析器可以被PHP或Python直接嵌入和使用,几乎没有开销。非常整洁!

Rust ? C

640?wx_fmt=png

为了在C里面使用Rust,只需要下面两个东西:

  • 一个静态库(.a文件)

  • 一个头文件(.h文件)

理论分析

要将Rust项目编译成静态库,crate类型属性必须包含staticlib值。让我们编辑一下Cargo.toml如下:

[lib]
name = "gutenberg_post_parser"
crate-type = ["staticlib"]

运行cargo build -release之后, 就会有libgutenberg_post_parser.a文件被生成到target/release/。完工!cargorustc使这一步非常容易。

现在轮到头文件了。它可以手动写成,但这样会非常枯燥而且容易过时即和源代码不同步。我们的目标是自动化生成。进入cbindgen

cbindgen可以用来生成Rust代码的C绑定。目前它主要被开发来支持创建WebRender的绑定,但是它还被设计得可以支持任何项目。

要安装cbindgen,编辑你的Cargo.toml文件,如下:

[package]
build = "build.rs"[build-dependencies]
cbindgen = "^0.6.0"

事实上,cbindgen有两种使用方式:独立命令行可执行程序,或者一个库。我喜欢使用库的方式,因为这让安装更简单。

注意我们已经指示Cargo用build.rs来构建项目。这个文件是一个很合适的地方来使用cbindgen来生成C头文件。我们来写一下!

extern crate cbindgen;fn main() {let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();cbindgen::generate(crate_dir).expect("Unable to generate C bindings.").write_to_file("dist/gutenberg_post_parser.h");
}

有了这些信息,cbindgen会扫描项目的源代码并且会自动的生成C头文件到dist/gutenberg_post_parser.h。稍后会细讲扫描部分,现在我们来快速的看看如何控制头文件中的内容。基于上面的代码片段,cbindgen会到CARGO_MANIFEST_DIR目录去找一个叫做cbindgen.toml的配置文件,也就是crate的根目录。我们看起来是这样的:

header = """
/*
Gutengerg Post Parser, the C bindings.Warning, this file is autogenerated by `cbindgen`.
Do not modify this manually.*/"""
tab_width = 4
language = "C"

它非常简洁且自描述。文档也把配置描述的很详细。

cbindgen要扫描代码,碰到有#[repr(C)]#[repr(size)] or #[repr(transparent)]修饰的structs或者enums会停下来,还有那些用 extern "C" 标记的公共函数。我们继续写:

#[repr(C)]
pub struct Slice {pointer: *const c_char,length: usize
}#[repr(C)]
pub enum Option {Some(Slice),None
}#[no_mangle]
pub extern "C" parse(pointer: *const c_char) -> c_void { … }

然后cbindgen的输出会是这样:

… header comment …typedef struct {const char *pointer;uintptr_t length;
} Slice;typedef enum {Some,None,
} Option_Tag;typedef struct {Slice _0;
} Some_Body;typedef struct {Option_Tag tag;union {Some_Body some;};
} Option;void parse(const char *pointer);

可以工作,非常棒!

注意有#[no_mangle]修饰Rust的parse函数。它指示编译器不要对这个函数重命名,因此这个函数在C语言的表示里面会保持和Rust相同的名字。

好了,这就是所有的理论基础。实战开始,我们有一个解析器需要绑定到C!

实战

我们要来绑定parse函数。这个函数的输出是我们要分析的语言的AST表示。回顾一下,我们原来的AST看起来是这样的:

pub enum Node<'a> {Block {name: (Input<'a>, Input<'a>),attributes: Option<Input<'a>>,children: Vec<Node<'a>>},Phase(Input<'a>)
}

这个AST是定义在Rust解析器里面的。而Rust的C绑定会转换这个AST到另外为C准备的structenum。Rust内部的类型不需要这个转换,只有对需要直接暴露到C语言的类型才是必须的。我们开始定义Node:

#[repr(C)]
pub enum Node {Block {namespace: Slice_c_char,name: Slice_c_char,attributes: Option_c_char,children: *const c_void},Phrase(Slice_c_char)
}

可以立刻想到的:

  • Slice_c_char模拟Rust的切片(看下面),

  • enum Option_c_char模拟Option (看下面),

  • children成员是*const c_void类型。它应该是*const Vector_Node(我们定义的Vector),但是Node的定义是基于Vector_Node的,相反也成立。循环定义的情况当前的cbindgen还不支持。因此它被定义为空指针,将在C里面做强制转换。

  • namespace 和 name成员原来在Rust中是一个元组。因为在元组在cbindgen里面没有对应的类型,因此我们这里用两个成员来代替。

我们来定义Slice_c_char

#[repr(C)]
pub struct Slice_c_char {pointer: *const c_char,length: usize
}

这个定义借用了Rust的Slices语意。主要的好处是Rust的slice绑定到这个结构的时候不需要copy。

我们来定义Option_c_char

#[repr(C)]
pub enum Option_c_char {Some(Slice_c_char),None
}

最后,我们需要定义Vector_NodeResult。他们都是非常接近Rust的模拟:

#[repr(C)]
pub struct Vector_Node {buffer: *const Node,length: usize
}#[repr(C)]
pub enum Result {Ok(Vector_Node),Err
}

好的,所有的类型都定义了。是时候开始写parse函数了:

#[no_mangle]
pub extern "C" fn parse(pointer: *const c_char) -> Result {…
}

这个函数在C语言里面接受一个指针。它由C分配代表了我们要分析的数据(也就是Gutenberg的博客文章):内存是在C语言里面分配的,Rust只负责解析。Rust出色的地方体现在:没拷贝,没克隆,没有混乱的内存,只有指向数据的指针会返回给C语言当作slices和数组。

工作流如下:

  • C里面第一件事情:检查指针不为空,

  • 基于这个指针用CStr重建输入。这个标准API对于从Rust的角度抽象C字符串非常有用。区别是C字符串以NULL为结束字节没有长度,然而Rust字符串有长度而不是NULL字节作为结束。

  • 运行解析器,转换AST到“C AST”

我们开始!

pub extern "C" fn parse(pointer: *const c_char) -> Result {if pointer.is_null() {return Result::Err;}let input = unsafe { CStr::from_ptr(pointer).to_bytes() };if let Ok((_remaining, nodes)) = gutenberg_post_parser::root(input) {let output: Vec =nodes.into_iter().map(|node| into_c(&node)).collect();let vector_node = Vector_Node {buffer: output.as_slice().as_ptr(),length: output.len()};mem::forget(output);Result::Ok(vector_node);} else {Result::Err}
}

Vector_Node里面只用到了指向output的指针,以及output的长度。这个转换是比较轻量的。

现在来看into_c函数。有写部分不会细讲;不是因为它太难而是有点重复。所有的代码都在这里可以找到。

fn into_c<'a>(node: &ast::Node<'a>) -> Node {match *node {ast::Node::Block { name, attributes, ref children } => {Node::Block {namespace: …,name: …,attributes: …,children: …}},ast::Node::Phrase(input) => {Node::Phrase(…)}}
}

我想展示namespace作为一个热身(nameattributes 和 Phrase 都非常类似),还会展示childen因为它处理void

先转换ast::Node::Block.name.0Node::Block.namespace

ast::Node::Block { name, …, … } => {Node::Block {namespace: Slice_c_char {pointer: name.0.as_ptr() as *const c_char,length: name.0.len()},…

目前还非常的直观。namespaceSlice_c_char类型。pointername.0切片的指针。lengthname.0的长度。处理其它的Rust切片这个过程一样。

children有点不一样,它需要下面的三步:

  • 把所有的childen作为C AST节点保存到Rust vector里面,

  • 转换这个Rust vector到一个合法的Vector_Node

  • 转换Vector_Node*const c_void pointer

ast::Node::Block { …, …, ref children } => {Node::Block {…children: {// 1. Collect all children as C AST nodes.let output: Vec =children.into_iter().map(|node| into_c(&node)).collect();// 2. Transform the vector into a Vector_Node.let vector_node = if output.is_empty() {Box::new(Vector_Node {buffer: ptr::null(),length: 0})} else {Box::new(Vector_Node {buffer: output.as_slice().as_ptr(),length: output.len()})}// 3. Transform Vector_Node into a *const c_void pointer.let vector_node_pointer = Box::into_raw(vector_node) as *const c_void;mem::forget(output);vector_node_pointer}

第一步是直观的的。

第二步,定义在没有节点时候的行为。换句话说,定义了什么是空Vector_Nodebuffer必须是值为NULL字节的原始指针,length也显然会是0. 不这样做,即使我在代码里面检查了buffer的长度,我依然碰到了严重的段错误。注意Vector_Node是通过Box::new在堆上分配的,它可以很容易的和C共享。

第三步,用Box::into_raw函数消费这个box并且返回一个封装了的原始指针,这个指针指向box拥有的数据。这里Rust不会释放任何东西,这是我们的职责(或者更严谨的说是C语言的职责)。然后·Box::into_raw·返回的·*mut Vector_Node·可以无成本转换为·*const c_void·。

最后,我们通过·mem::forget·(你已经看到这个系列了的当前位置了,很大可能性已经知道它的作用了)指示编译器当output离开作用域的时候不要释放它

对我自己来讲,我花了好几个小时去理解为什么我的指针会得到随机地址,或者指向NULL数据。虽然得到的最终代码看起来比较的简单易读,但是在知道如何做到这个之前却不是那么显然的。

这就是Rust部分所有的内容。下一个部分我们有展示用C代码来调用Rust,以及如何把所有的东西编译到一起。

C ? 可执行程序

640?wx_fmt=png

既然Rust部分已经就绪,的要写C的部分作为调用方。

最小可工作示例

我们快速的写点代码看看是否可以链接和编译:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "gutenberg_post_parser.h"int main(int argc, char **argv) {FILE* file = fopen(argv[1], "rb");fseek(file, 0, SEEK_END);long file_size = ftell(file);rewind(file);char* file_content = (char*) malloc(file_size * sizeof(char));fread(file_content, 1, file_size, file);// Let's call Rust!Result output = parse(file_content);if (output.tag == Err) {printf("Error while parsing.\n");return 1;}const Vector_Node nodes = output.ok._0;// Do something with nodes.free(file_content);fclose(file);return 0;
}

为了保持代码的简洁,我在示例代码中没有做任何的错误处理。如果你感兴趣,可以到这里找到所以的代码。

代码里面做了什么?第一个值得注意的是 #include "gutenberg_post_parser.h",这个是由cbindgen自动生成的头文件。

然后是从argv[1]得到的filename用来读取博客文章到parse函数。来自Rust的parse函数只喜欢Result 和 Vector_Node类型

Rust的enum Result { Ok(Vector_Node), Err }编译到C语言看起来是这样的:

typedef enum {Ok,Err,
} Result_Tag;typedef struct {Vector_Node _0;
} Ok_Body;typedef struct {Result_Tag tag;union {Ok_Body ok;};
} Result;

没有必要说Rust版更容易阅读,更紧凑,但这不是重点。为了检查Result是否包含一个OK或者Error,我们必须检查tag成员变量,就像我们以前检查output.tag == Err。要得到Ok的内容,我们用output.ok._0 (_0 是Ok_Body的成员变量).

我们用clang来编译!假设上面的代码和gutenberg_post_parser.h文件在同一个目录,也就是在dist/目录。因此:

$ cd dist
$ clang \# Enable all warnings. \-Wall \# Output executable name. \-o gutenberg-post-parser \# Input source file. \gutenberg_post_parser.c \# Directory where to find the static library (*.a). \-L ../target/release/ \# Link with the gutenberg_post_parser.h file. \-l gutenberg_post_parser \# Other libraries to link with.-l System \-l pthread \-l c \-l m

就这些!我们最终得到一个gutenberg-post-parser可执行文件,它能运行C和Rust。

更多的细节

在原始源代码中,可以找到一个在stdout上打印整个AST的递归函数,即print(够原始吧,不是吗?)。下面是Rust语法和C语法之间的一些并列比较。

Rust里的Vector_Node :

pub struct Vector_Node {buffer: *const Node,length: usize
}

C里的Vector_Node

 typedef struct {const Node *buffer;uintptr_t length;} Vector_Node;

因此,要分别读取节点数(数组的长度)和C中的节点,必须这样写:

const uintptr_t number_of_nodes = nodes->length;for (uintptr_t nth = 0; nth < number_of_nodes; ++nth) {const Node node = nodes->buffer[nth];
}

这几乎是惯用的C代码! 节点在C中定义为:

typedef enum {Block,Phrase,
} Node_Tag;typedef struct {Slice_c_char namespace;Slice_c_char name;Option_c_char attributes;const void* children;
} Block_Body;typedef struct {Slice_c_char _0;
} Phrase_Body;typedef struct {Node_Tag tag;union {Block_Body block;Phrase_Body phrase;};
} Node;

因此,一旦获取了节点,就可以编写以下代码来检测其类型:

if (node.tag == Block) {// …
} else if (node.tag == Phrase) {// …
}

让我们先关注一下Block,然后打印namespacename,他们之间用斜杠(/)分隔:

const Block_Body block = node.block;const Slice_c_char namespace = block.namespace;
const Slice_c_char name = block.name;printf("%.*s/%.s\n",(int) namespace.length, namespace.pointer,(int) name.length, name.pointer
);

printf中特殊的%.s形式中的允许根据字符串的长度和指针打印字符串。

我觉得看看如何把children节点从void转换到Vector_Node比较有趣,只需要一行:

const Vector_Node* children = (const Vector_Node*) (block.children);

我想这就是所有的细节!

测试

我认为,看看如何直接用Rust对C绑定进行单元测试也很有趣。要模拟C绑定,首先,输入必须是C格式的,所以字符串必须是C字符串。我更喜欢写一个宏来做这个事情:

macro_rules! str_to_c_char {($input:expr) => ({::std::ffi::CString::new($input).unwrap()})
}

第二,相反的方向:parse函数返回C语言的数据,因此需要将它们转换回Rust。同样,我更喜欢为此编写一个宏:

macro_rules! slice_c_char_to_str {($input:ident) => (unsafe {::std::ffi::CStr::from_bytes_with_nul_unchecked(::std::slice::from_raw_parts($input.pointer as *const u8,$input.length + 1).to_str().unwrap())})
}

好吧!最后一步是编写单元测试。写一个短语作为被测试对象的示例,对于Block的想法是一样的,但是前者的代码更简洁。

#[test]
fn test_root_with_a_phrase() {let input = str_to_c_char!("foo");let output = parse(input.as_ptr());match output {Result::Ok(result) => match result {Vector_Node { buffer, length } if length == 1 =>match unsafe { &*buffer } {Node::Phrase(phrase) => {assert_eq!(slice_c_char_to_str!(phrase), "foo");},_ => assert!(false)},_ => assert!(false)},_ => assert!(false)}
}

这里发生了什么?输入和输出都准备好了。前者是C字符串“foo”。后者是解析的结果。然后有一个匹配来验证AST。Rust非常有表达力,这个测试就是一个很好的例子。进入Vector_Node分支,当且仅当向量长度为1时,表示为length== 1时,然后将短语的内容转换为Rust字符串,并用常规的assert_eq!宏进行比较。

注意,在本例中buffer类型为*const Node,因此它表示向量的第一个元素。如果我们想访问下一个元素,我们需要使用Vec::from_raw_parts函数来获得适当的Rust API来操作这个向量。

#结论

我们已经看到Rust可以很容易地嵌入C中。在本例中,Rust已编译为一个静态库和一个头文件;前者是原生的Rust工具,后者是使用cbindgen自动生成的。

用Rust编写的解析器操作一个由C分配和拥有的字符串。Rust只将这个字符串的指针(作为切片)返回给C,然后C就可以轻松地读取这些指针了。惟一棘手的部分是Rust在堆上分配了一些C必须释放的数据(比如节点的数组)。不过,本文省略了“free”部分:它并不代表很大的挑战,而C开发人员可能已经习惯了这种情况。

Rust不使用垃圾收集器这一事实使它成为这些场景的完美候选语言。这些绑定背后的故事实际上都是关于内存的:谁分配了什么,内存中数据的形式是什么。Rust有一个#[repr(C)]装饰器来指示编译器使用C内存布局,这使得C绑定对于开发人员来说非常简单。

我们还看到,C绑定可以在Rust内部进行单元测试,并与cargo测试一起运行。

cbindgen是这次冒险的一个宝贵的伙伴,通过自动生成头文件,它将代码的更新和维护简化为build.rs脚本。

在性能方面,C应该比Rust有相似的结果,非常快。我没有运行基准测试来验证这个声明,它纯粹是理论上的。它可以作为下一篇文章的主题!

现在我们已经成功地将Rust嵌入到C中,一个全新的世界向我们打开了!下一集将把Rust作为一个本地扩展(用C编写)推向PHP的世界。我们走吧!

这篇关于从Rust到远方:C星系的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1085902

相关文章

【Rust练习】12.枚举

练习题来自:https://practice-zh.course.rs/compound-types/enum.html 1 // 修复错误enum Number {Zero,One,Two,}enum Number1 {Zero = 0,One,Two,}// C语言风格的枚举定义enum Number2 {Zero = 0.0,One = 1.0,Two = 2.0,}fn m

linux中使用rust语言在不同进程之间通信

第一种:使用mmap映射相同文件 fn main() {let pid = std::process::id();println!(

第二十四章 rust中的运算符重载

注意 本系列文章已升级、转移至我的自建站点中,本章原文为:rust中的运算符重载 目录 注意一、前言二、基本使用三、常用运算符四、通用约束 一、前言 C/C++中有运算符重载这一概念,它的目的是让即使含不相干的内容也能通过我们自定义的方法进行运算符操作运算。 比如字符串本身是不能相加的,但由于C++中的String重载了运算符+,所以我们就可以将两个字符串进行相加、但实际

【Rust光年纪】Rust 机器人学库全景:功能、安装与API概览

机器人学+Rust语言=无限可能:六款库带你开启创新之旅! 前言 随着机器人技术的快速发展,对于机器人学领域的高效、可靠的编程语言和库的需求也日益增加。本文将探讨一些用于 Rust 语言的机器人学库,以及它们的核心功能、使用场景、安装配置和 API 概览,旨在为机器人学爱好者和开发人员提供参考和指导。 欢迎订阅专栏:Rust光年纪 文章目录 机器人学+Rust语言=无限可能:

第二十二章 rust数据库使用:sea-orm详解

注意 本系列文章已升级、转移至我的自建站点中,本章原文为:rust数据库使用:sea-orm详解 目录 注意一、前言二、项目管理三、迁移文件四、实体文件五、业务使用 一、前言 只要开发稍微大型一点的项目,数据库都是离不开的。 rust目前并没有特别成熟的数据库框架,sea-orm这个框架是我目前所看到的成熟度最高的一个,并且仍在积极开发中。 所以本文将以sea-orm框

Rust使用之【宏】

一、简单使用clap clap = { version = "4.5.17", features = ["derive"] } 其中,什么是features = ["derive"]:表示你希望在添加 clap 依赖时启用 derive 特性。这通常意味着你希望使用 clap 的派生(derive)宏功能,这些功能可以简化创建命令行接口的代码。例如,derive 特性可以让你使用 #[der

第二十一章 rust与动静态库的结合使用

注意 本系列文章已升级、转移至我的自建站点中,本章原文为:rust与动静态库的结合使用 目录 注意一、前言二、库生成三、库使用四、总结 一、前言 rust中多了很多类型的库,比如前面章节中我们提到基本的bin与lib这两种crate类型库。 如果你在命令行执行下列语句: rustc --help 那么你将能找到这样的内容: --crate-type [bin|li

Rust的常数、作用域与所有权

【图书介绍】《Rust编程与项目实战》-CSDN博客 《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com) Rust到底值不值得学,之一  -CSDN博客 Rust到底值不值得学,之二-CSDN博客 Rust的数据类型-CSDN博客 3.7  常数的数据类型 在Rust语言中,变量有类型,常量也有类型。我们知道,在定义const常量的时候,就要

搭建Rust的开发环境

目标 我没有使用过Rust,但我听说它是一个可靠的语言,可以保证内存安全和线程安全。我对此很有兴趣,就想试一试这个语言。 在官网上有介绍他们所推荐的编辑器: 我将选择 Visual Studio Code (关于 【Visual Studio】和【Visual Studio Code】的区别:【VS】是完整的集成开发环境,而【VSCode】只算的上是一个相对轻量级的文本编辑器,并附带一些便捷

Rust的数据类型

【图书介绍】《Rust编程与项目实战》-CSDN博客 《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com) Rust到底值不值得学,之一  -CSDN博客 Rust到底值不值得学,之二-CSDN博客 3.5  数据类型的定义和分类 在Rust编程中,所谓数据类型,就是对数据存储的安排,包括存储单元的长度(占多少字节)以及数据的存储形式。不同的