发布时间:2025-06-09
浏览次数:0
作者 | 于玉龙
本文旨在对Rust编程语言的相关知识进行深入剖析,期望通过本文的阐述,能为对该领域感兴趣的编程人员带来有益的经验和助力。
关于Rust
Rust是一种类型严格、编译时运行且内存安全的编程语言。最初,Rust的早期版本是由一家基金会中一位名叫Hoare的员工发起的私人项目。自2009年起,该项目得到了赞助者的支持,并逐渐发展壮大。到了2010年,Rust实现了自举功能——即用Rust语言构建了Rust编译器。
将Rust编程语言融入新一代浏览器排版引擎Servo的开发中,自2017年起,Servo的CSS处理模块便已融入其中。
Rust原本定位为一款内存管理安全的编程语言,其设计初衷在于取代C++或C,以构建诸如操作系统、浏览器等大型底层项目。然而,由于这种特性,前端领域也开始关注并采纳了这门语言,进而推动了其生态的逐步繁荣。
内存安全——Rust的一大杀手锏
众所周知,在目前流行的编程语言中,通常可以划分为两大类。一类是具备自动垃圾回收功能的,例如Java、等;而另一类则是C++和C,用户必须自行进行内存管理。
大部分语言的内存模型都是大同小异的。
当程序运行起来,每个变量对应的数值将依次被推入栈中,而每当程序完成某一作用域的操作后,相应的变量值也会随之从栈中移除。由于栈这种数据结构遵循的是后进先出的原则,它完美地映射了编程语言中作用域的规则——最外围的作用域先被定义,随后才结束。然而,由于栈的特性,它无法在中间位置插入数据,故而只能存放那些一旦定义后,所占用的内存空间便不再发生变化的数值,例如整数(int)和字符(char),或者是长度固定的数组。至于那些长度可变的数组、可变长度的字符串等,则无法被存入栈中。
在编程语言需要占用不确定大小的内存空间时,它会向操作系统提出请求,操作系统随后分配相应大小的内存区域,并将该区域的内存地址——即指针——传递给程序。这样一来,编程语言便能够将这些数据存储在堆中,同时将指针保存在栈中。这是因为指针的长度是固定的,无论是32位程序还是64位程序,它们的指针长度分别为32bit和64bit。
栈内数据无需进行内存操作,在程序运行过程中,一个变量是否还有价值,可以轻易地被判定——一旦该变量的作用范围终止,便无法再获取其值,从而确定该变量已无用。仅需在作用域的设定与终结时,通过持续地进行入栈和出栈操作,便能有效管理栈的内存空间,无需程序员过多干预。
然而,堆中的数据却无法保证,这是因为程序获取的仅是内存的指针,而实际的内存区域并不位于栈中,因此不能随着栈的自动销毁而消失。此外,程序在栈内存指针变量被销毁时,也不能自动清理指针所指的空间——这是因为可能存在多个变量保存的指针都指向同一内存区域,一旦清理该内存区域,可能会引发未预料到的后果。
鉴于这一点,部分程序内置了一套极其繁复的垃圾回收算法,此类算法如引用计数法,能够追踪一个内存区域的指针被存储在多少个变量之中;一旦引用计数降至零,便意味着指向该内存区域的指针均已失效,此时该内存块便可以予以释放。相对而言,另一些程序则需人工进行内存管理,任何在堆中分配的内存空间,都必须由人工进行清理。
这两种方案各有利弊,第一种方案使得程序不得不携带一个包含GC算法的模块,这直接导致了程序体积的增大;而第二种方案,则可能引发内存安全的问题,换句话说,内存管理的责任转移到了程序员身上,他们的技术水平在很大程度上决定了代码的安全性。如果程序员忘记回收内存,程序占用的内存会持续增加;如果回收操作出现错误,可能会误删不应删除的数据。除此之外,还有可能因指针操作不当,导致数据溢出到其他区域,进而修改了不应被修改的数据等问题。
而Rust则采取了一种全新的内存管理方式。此方法可简要归纳为:程序员与编译器建立某种共识,程序员需遵循此共识编写代码。一旦程序员依照此共识编写代码,内存区块是否仍在使用便一目了然,其清晰程度无需程序运行,编译阶段即可得知。据此,编译器可于代码的指定位置插入内存回收指令,从而实现内存的回收。换言之,Rust在本质上通过限制引用的运用,成功避开了难以确定某块地址是否仍在被使用的情形,而对于其他所有情况,判断起来都变得相当直观,以至于即便是非专业程序员,仅需借助编译器,便能够轻松地进行判断。
这样的一大好处是:
实现原理
Rust的内存安全保障体系堪称独树一帜,其具备一套简洁明了、易于掌握的机制,即所有权系统。该系统内包含了两个至关重要的概念,即所有权与借用。
所有权
所有数据,无论是指针还是其他类型,都必须与一个变量相连接,这样该变量便被赋予了该数据的所有权。以以下代码为例,变量str便拥有了字符串“hello”的所有权。
let str = "hello"
随着str所在的作用域范围结束,其存储的值将自动被清除,此时str将不再具备任何有效性。这种现象与绝大多数流行编程语言的处理方式相符,并无任何异常之处。这一机制也相对直观易懂。
然而,需留意Rust对可变长度字符串与不可变长度字符串进行了区分,前文所述即为不可变长度字符串,鉴于其长度固定,故可存于栈上。因此,接下来的代码段能够正常运行,这与其他绝大多数主流编程语言的表现一致。
let str = "hello world";
let str2 = str;
println!("{}", str);
println!("{}", str2);
然而,若将一段存储于堆内存中、其长度可以变化的字符串引入,我们再来观察下相同的代码:
fn main() {
let str = String::from("hello world");
let str2 = str;
println!("{}", str);
println!("{}", str2);
}
此时,我们会惊讶地发现,代码报错了。为什么呢?
由于在第一段代码中,变量str的值存储于栈中,其所代表的是"hello world"这一系列字符。因此,当执行str2=str时,实际上是在创建一个新的变量str2,它同样承载着与str完全相同的字符串序列,这一过程即是“内存的复制”。这两个变量分别独立地拥有“hello world”这一数据的控制权,但它们所持有的“hello world”并非同一个实体。
在第二段代码中,我们获取到的str,实际上仅仅是一个指向特定内存区域的指针。当我们执行str2=str时,实际上是将这个指针指向的内存地址的值赋给了str2。在其他编程语言中,这样的操作很可能不会出现问题,然而,str和str2将共享相同的内存地址。因此,当修改str时,str2的值也会随之改变。在rust语言中,一个特定的值只能被一个变量所绑定,换言之,某个变量独享该值的所有权,就好比同一物品在同一时刻只能归一人所有。当执行str2=str的操作时,原本由str保存的地址值便不再属于str,而是转归str2所有,这种现象被称为【所有权转移】。因此,str便失去了其有效性,若我们尝试使用一个无效的值,自然会导致错误发生。
以下这些情况都能导致所有权转移:
上文提到的赋值操作:
定义字符串str,其值为从"hello world"获取;将str的值赋给str2;此时,str不再拥有该字符串的所有权。
将一个值传进另一个作用域,比如函数:
如此一来,我们便能轻易察觉,同一内存区域地址只能由一个变量存储,若该变量超出其作用范围,导致无法读取,那么该内存地址便将永久失去访问权限,进而使得该内存区域得以释放。这一判断过程相当简便,完全可以由编译器在静态检查阶段自动完成。所以rust可以很简单的实现内存安全。
然而,这种表述方式实在是对人类极不友好的,尽管它确实解决了内存安全的问题intellij idea golang plugin,但使用起来并不便捷。例如,当我需要将一个字符串传入某个方法进行一系列逻辑处理,处理完毕后,我仍希望保留并读取这个字符串,就如同以下代码所示:
fn main() {
let mut str1 = String::from("hello world"此处的mut仅作为标识,表明该变量属于可变类型,非固定不变的常量。
add_str(mut str1, "!!!");
println!("{}", str1);
}
str_1.push_str(str_2);
}
左右滑动查看完整代码
我们打算对变量str进行一系列操作,包括在其后附加三个感叹号并输出,但这样的代码存在错误。这是因为一旦str被传递给方法,其所有权便转移至方法内部的变量str_1,原变量str便失去了所有权,因此无法再对其进行操作。这种情况在编程中颇为常见,单纯的所有权机制使得问题变得复杂。为此,Rust语言引入了另一种机制,即【引用和借用】,用以解决此类问题。
借用:
尽管一个值只能被一个变量所独占,但类比于个人可以将自己的物品借与他人使用,且可以多次借给不同的人,变量同样能够将其所持有的值借出,只需对上述代码进行微调即可。
fn main() {
let mut str1 = String::from("hello world");
add_str(&mut str1, "!!!");
println!("{}", str1);
}
str_1.push_str(str_2);
}
接收到的已不是mut str类型,而是&mut str1,这等于是从str1那里临时借用了一份数据进行使用。然而,数据的实际所有权依然属于str1。至于内存区块的回收条件,依旧遵循这样的规则:当str1所在的作用域执行完成,str1所保存的内存地址随之出栈并被销毁。
这两种机制所形成的核心在于:对内存的引用计数变得极为简便,只要内存地址所指向的变量存在于堆中,其引用计数即为1;若不在,则计数为0,仅此两种情形。这样一来,多个变量指向同一内存地址的情况不复存在,从而显著降低了引用计数GC算法的复杂度。将运行时复杂度降至无需,静态检查阶段便能够识别出所有需要进行垃圾回收的时刻,进而实施垃圾回收操作。
Rust的其他特性
Rust融合了那些特性,从而成为C++的绝佳替代品。在当前的前端领域,Rust的应用主要集中在两个方向:一是利用Rust构建性能更优的前端工具,二是将其作为WASM的编程语言,最终编译成能在浏览器中运行的WASM模块。
高性能工具
在此之前,若前端开发者想要打造一款性能卓越的工具,gyp成为了他们的首选。他们需用C++进行编程,借助gyp进行编译,从而生成可供调用的API。众多广为人知的库,如saas-,也都是采用这种方式实现的。然而,在多数情形下,前端领域的大多数工具对性能并不重视,它们直接采用进行编写,例如Babel等,这其中的一个重要原因是C++的入门难度较高,仅是C++的数十个版本特性,就足以让人投入大量时间去学习,而且学成之后,还需要丰富的开发经验才能掌握如何更有效地进行内存管理、防止内存泄漏等问题。Rust独具特色,其年轻的生命力显而易见,它没有经历数十个版本的迭代,却拥有与npm相媲美的现代化包管理工具。更重要的是,Rust在内存管理方面表现出色,杜绝了内存泄漏的问题。即便如此,Rust的历史并不悠久,C++也能借助它来编写扩展,然而在前端领域,却涌现出了众多由Rust编写的性能卓越的工具。例如:
前端技术日益繁复,因此我们必然逐步寻求性能更优的工具链支持;或许在不久的将来,我们便能目睹采用swc与Rome正式版的项目在生产环境中稳定运行。
WASM
此外,随着WASM的问世,前端领域也在积极寻求一种能够最佳支持WASM的编程语言,目前看来,Rust似乎是最合适的选择。WASM不认可包含运行时的编程语言,因为此类语言在转换为WASM格式时,不仅会包含我们编写的业务代码,还会引入运行时代码,诸如GC等机制,这显著增加了包的大小,对用户体验不利。去除带运行时的语言后,前端可选的语言种类受限,在C++和Rust中,Rust的优势使得前端开发者更倾向于选择Rust。Rust同样在这一领域给予了良好的帮助,其官方编译器能够将Rust代码转换为WASM格式。结合wasm-pack这一便捷工具,前端开发者能够迅速构建wasm模块。以下是一个简单的示例,以下代码片段正是我从之前提到的swc中提取出来的。
#![deny(warnings)]
允许忽略未使用的单元。
// 引用其他的包或者标准库、外部库
use std::sync::Arc;
use anyhow::{Context, Error};
use once_cell::sync::Lazy;
use swc::{
配置包括错误格式、JavaScript最小化选项、基本配置、解析选项以及源映射配置。
尝试使用处理器,编译器。
};
引入swc_common模块中的comments::Comments、FileName、FilePathMapping和SourceMap类。
采用swc_ecmascript模块中的ast子模块,具体包括EsVersion和Program两个类型。
// 引入wasm相关的库
use wasm_bindgen::prelude::*;
通过wasm_bindgen宏的应用,该方法的名称在编译为wasm格式后变更为transformSync。
// TS的类型是transformSync
#[wasm_bindgen(
js_name = "transformSync",
typescript_type = "transformSync",
skip_typescript
)]
#[allow(unused_variables)]
创建一个函数,此函数由于是公开的,故可供外界访问。其主要功能在于:将较高版本的JavaScript代码转换为适用于较低版本的JavaScript代码。
// 具体的内部逻辑我们完全不去管。
pub fn transform_sync(
s: &str,
opts: JsValue,
实验性插件字节解析器:JavaScript值类型
) -> Result {
设置console_error_panic_hook的触发机制,确保仅执行一次。
let c = compiler();
#[cfg(feature = "plugin")]
{
if实验性插件字节解析器。is_object() {
引入JavaScript系统中的Array、Object和Uint8Array模块。
此做法可能相当低效,将每个转换过程都包含在内。
对插件字节进行反序列化处理。
letplugin_bytes_resolver_object:该对象为Object类型,属于实验性的plugin_bytes_resolver。
.try_into()
.expect(解算器应当是一个JavaScript对象。);
swc_plugin_runner模块下的cache函数负责初始化插件模块缓存,且此操作仅执行一次。
let
for entry in entries.iter() {
let entry: Array = entry
将对象转换成指定类型。
.expect(解析对象缺失关键或值信息。);
let name: String = entry
.get(0)
将对象转换为字符串形式。
.expect(解密器密钥必须为字符串形式。);
let buffer = entry.get(1);
在rustwasm的wasm-bindgen项目中,存在一个编号为2017的问题,该问题已被禁止进行任何形式的修改。#issue-573013044
我们或许在稍后阶段会转而采用https://github.com/cloudflare/serde-wasm-bindgen。
let data = if JsCast::is_instance_of::(&buffer) {
JsValue::通过将buffer引用转换为Array类型,进而生成。
} else {
buffer
};
let bytes: Vec = data
执行.into_serde()操作
.expect(无法从插件解析器读取字节。);
在此处,我们需遵循规定,不得擅自更改。'inject'外部加载的字节已存入缓存,因此,
剩余的插件运行器执行路径仍然能够正常运作,其功能表现与之前无异。
嵌入式运行时之间具有诸多相似之处。
swc_plugin_runner的cache模块中的PLUGIN_MODULE_CACHE对象,仅存储一次,将name的引用和bytes数据存储进去。
}
}
}
let opts: Options = opts
.into_serde()
.context("failed to parse options")
对错误进行捕获,并将捕获到的错误转换为常规格式错误,随后执行相应的操作。
let
try_with_handler(
c.cm.clone(),
swc::HandlerOpts {
采用系统默认设置,执行default()函数。
},
|handler| {
c.run(|| {
let fm = c.cm.new_source_file(
if opts.filename.is_empty() {
文件名:匿名
} else {
FileName由opts.filename的克隆版本转换而来,并执行了into()操作。
},
s.into(),
);
let out = c
执行过程:对文件进行JavaScript处理,调用fm参数,处理handler函数,并引用opts选项。
.context(未能成功处理输入文件)?;
将输出数据转换为JsValue类型,并在上下文中处理。"failed to serialize json")
})
},
)
在处理错误时,若遇到异常,应将其转换为错误格式,并通过convert_err函数进行转换。
}
左右滑动查看完整代码
显而易见,一旦拥有一个Rust库,将其转换成WASM的过程就变得相当简便,读者不妨亲自尝试一番intellij idea golang plugin,对比C++的WASM转换,会发现Rust的整个操作流程要轻松许多。
有没有什么问题?
尽管我先前对Rust的优点进行了多方面的阐述,但在实际学习过程中,我却遭遇了不少挫折,其中一大原因便是Rust的独树一帜。
以一个十分直观的例子来说明,在多数编程语言中,对变量和常量的定义方式各有不同,有的是通过不同的关键字来区分,例如中的let与const,Go语言中的const与var;有的则是默认将声明的变量视为变量,而要将常量特别声明,比如在Java中,若在变量前加上final关键字,该变量就会被识别为常量;而在Rust中,情况则恰好相反,默认声明的都是常量,若要使用变量,则需要特别进行声明,如使用let a=1时得到的是常量,而使用let mut a=1时才构成变量。
上述所提及的,Rust的独到之处颇多,尽管这些特点大多只是设计理念的差异,并无明显的好坏之分,然而这样的设计确实给其他语言的开发者带来了一定的思维负担。
依据我的学习经历,Rust这门语言的学习难度不容小觑,实际上,它的学习曲线可能并不比C++来得平缓。在社区中,甚至有观点认为,若想真正掌握Rust的精髓,先学习C++是必要的,否则很难完全体会到Rust那种优雅之处。对于有志于学习Rust的同学来说,做好心理准备是必不可少的。
于玉龙
腾讯云开发者社区“技思广益·腾讯技术人原创集”一书的作者系腾讯前端开发工程师。他毕业于湘潭大学,现正担任腾讯医疗健康工作室医疗SaaS产品前端开发一职,同时负责团队内部前端工具链的构建与维护工作。
点击下方视频
关注【51CTO技术栈】视频号
如有侵权请联系删除!
Copyright © 2023 江苏优软数字科技有限公司 All Rights Reserved.正版sublime text、Codejock、IntelliJ IDEA、sketch、Mestrenova、DNAstar服务提供商
13262879759
微信二维码