C语言stdlid是什么函数,从Rust到远方:C星系

这篇博客介绍了如何将Rust编译为C语言静态库,以便在C环境中使用Rust代码。通过创建C语言绑定,Rust编写的解析器可以在PHP或Python等语言中直接调用,实现跨语言交互。文章详细阐述了使用cbindgen自动生成C头文件的过程,并展示了如何处理Rust中的类型转换,确保在C中正确表示。最后,演示了如何在C代码中调用Rust解析器并整合所有组件。
摘要由CSDN通过智能技术生成

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

WebAssembly 星系

ASM.js星系

C星系(当前这一集)

PHP星系,以及

NodeJS 星系

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

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

5cd54787b030d506561372797bc1c255.png

Dennis Ritchie, C语言的发明者.

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

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

adf24a4941b16508caca3c2d691589e5.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/。完工!cargo和rustc使这一步非常容易。

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

Block {

name: (Input, Input),

attributes: Option>,

children: Vec>

},

Phase(Input)

}

这个AST是定义在Rust解析器里面的。而Rust的C绑定会转换这个AST到另外为C准备的struct和enum。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_Node和Result。他们都是非常接近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(node: &ast::Node) -> Node {

match *node {

ast::Node::Block { name, attributes, ref children } => {

Node::Block {

namespace: …,

name: …,

attributes: …,

children: …

}

},

ast::Node::Phrase(input) => {

Node::Phrase(…)

}

}

}

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

先转换ast::Node::Block.name.0到Node::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()

},

目前还非常的直观。namespace是Slice_c_char类型。pointer是name.0切片的指针。length是name.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_Node。buffer必须是值为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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值