当前位置:   article > 正文

Rust 和 WebAssembly 的世界,前端开发学习教程_rust webassembly 开发

rust webassembly 开发

Rust

====

Rust 语言是一种高效、可靠的通用高级语言。其高效不仅限于开发效率,它的执行效率也是令人称赞的,是一种少有的兼顾开发效率和执行效率的语言。Rust 是一种 预编译静态类型ahead-of-time compiled)语言,这意味着你可以编译程序,并将可执行文件送给其他人,他们甚至不需要安装 Rust 就可以运行。如果你给他人一个 .rb.py.js 文件,他们需要先分别安装 Ruby,Python,JavaScript 实现(运行时环境,VM)。

中文学习资源

https://kaisery.github.io/trpl-zh-cn/ch01-01-installation.html


以上摘抄自官方文档等学习资源 下面不逐个介绍Rust的语法与编译方式,主要介绍一些我认为的Rust语言的一些有意思的特点与设计思想。

  1. Rust中变量默认是不可变的(immutable)变量不可变可以说是一种规范,可以帮助我们更加直观的追寻数据的变化状态。例如使用react的PureComponent 或者 memo只会对行新旧数据的浅层比对,由于 JS 引用赋值的原因,这种方式仅仅适用于无状态组件或者状态数据非常简单的组件,对于大量的应用型组件,它是无能为力的,所以在编写的时候会考虑使用immutable +memo 浅层比对。

举个最简单的例子

// 可变,arr新增

arr.push(item)

// 不可变

arr = […arr,item]

  1. 完善的类型系统(和Typescript极其相似),但是在某些方面比Typescript更加的细,例如整形可以分为无符号,有符号的8-bit,16-bit,32-bit,64-bit,128-bit。

// 无符号的32位整形

let guess: u32 = “42”.parse().expect(“Not a number!”);

  1. 语言的集大成者,既有Javascript的灵活,又有C/C++的编译加持。

举一个体现其灵活的例子

let x = 5;

let y = {

let x = 3;

x + 1

};

println!(“The value of y is: {}”, y);

在上面的例子中Rust使用**;**来判断该句子是表达式,还是一个语句。

  1. 严谨的,灵活的控制流

// 会报错

fn main() {

let number = 3;

if number {

println!(“number was three”);

}

}

// 形如以下的赋值语句是完全有效的

let condition = true;

let number = if condition {

5

} else {

6

};

  1. 独特的内存管理方式,区别于垃圾回收机制(javascript)和亲自分配和释放内存(C/C++),Rust采用了另外一种管理操作系统内存的方式:通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查。在运行时,所有权系统的任何功能都不会减慢程序。

如果内存的操作分为以下两部分:

  • 必须在运行时向操作系统请求内存。

  • 需要一个当我们处理完 String 时将内存返回给操作系统的方法。

第一步大同小异,而对于第二步的处理就是百花齐放了,对于Rust而言

  1. Rust 中的每一个值都有一个被称为其 所有者owner)的变量。
  1. 值在任一时刻有且只有一个所有者。
  1. 当所有者(变量)离开作用域,这个值将被丢弃。

为了保持运行时的高效,Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何 自动 的复制可以被认为对运行时性能影响较小。对于此,Rust采用了一个规则,禁止把引用堆空间的栈空间变量改变(栈空间上的值类型可以直接引用),因为Rust 不需要在被首次分配空间的变量离开作用域后清理任何东西。

let s1 = String::from(“hello”);

let s2 = s1;

println!(“{}, world!”, s1);

// 会报错

let s1 = String::from(“hello”);

let s2 = s1.clone();

println!(“s1 = {}, s2 = {}”, s1, s2);

// 需要手动克隆

那么当所有者(变量)离开作用域,这个值将被丢弃这句话怎么理解呢?看下面的图,这种操作既不像浅拷贝shallow copy)和 深拷贝deep copy),那么拷贝指针、长度和容量而不拷贝数据可能听起来像浅拷贝。不过因为 Rust 同时使第一个变量无效了,这个操作被称为 移动move),但是注意一点对于值类型,Rust会直接拷贝,而不是进行移动,所以对于值类型(整形等),有函数调用它之后,仍然可以使用。

fn main() {

let s = String::from(“hello”);  // s 进入作用域

takes_ownership(s);             // s 的值移动到函数里 …

// … 所以到这里不再有效

let x = 5;                      // x 进入作用域

makes_copy(x);                  // x 应该移动函数里,

// 但 i32 是 Copy 的,所以在后面可继续使用 x

} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,

// 所以不会有特殊操作

fn takes_ownership(some_string: String) { // some_string 进入作用域

println!(“{}”, some_string);

} // 这里,some_string 移出作用域并调用 drop 方法。占用的内存被释放

fn makes_copy(some_integer: i32) { // some_integer 进入作用域

println!(“{}”, some_integer);

} // 这里,some_integer 移出作用域。不会有特殊操作

变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过 drop 被清理掉,除非数据被移动为另一个变量所有。

fn main() {

let s1 = gives_ownership();         // gives_ownership 将返回值

// 移给 s1

let s2 = String::from(“hello”);     // s2 进入作用域

let s3 = takes_and_gives_back(s2);  // s2 被移动到

// takes_and_gives_back 中,

// 它也将返回值移给 s3

} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,

// 所以什么也不会发生。s1 移出作用域并被丢弃

fn gives_ownership() -> String {             // gives_ownership 将返回值移动给

// 调用它的函数

let some_string = String::from(“hello”); // some_string 进入作用域.

some_string                              // 返回 some_string 并移出给调用的函数

}

// takes_and_gives_back 将传入字符串并返回该值

fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

a_string  // 返回 a_string 并移出给调用的函数

}

在每一个函数中都获取所有权并接着返回所有权有些啰嗦。如果我们想要函数使用一个值但不获取所有权该怎么办呢?这里就需要引用和借用(可以理解为c里面的指针)。

fn main() {

let s1 = String::from(“hello”);

let len = calculate_length(&s1);

println!(“The length of ‘{}’ is {}.”, s1, len);

}

fn calculate_length(s: &String) -> usize {

s.len()

}

// 尝试修改,那指定是不行的

fn main() {

let s = String::from(“hello”);

change(&s);

}

fn change(some_string: &String) {

some_string.push_str(“, world”);

}

函数调用了引用类型的引用,在函数体中使用该变量被称之为借用,那么又有一个问题了,你不让我改,我就是想改,诶,就是玩!那么这时候需要引入一个新的概念:可变引用

fn main() {

let mut s = String::from(“hello”);

change(&mut s);

}

fn change(some_string: &mut String) {

some_string.push_str(“, world”);

}

不过可变引用有一个很大的限制:在特定作用域中的特定数据只能有一个可变引用,并且可变引用和不可变引用不应该同时存在(这两是互斥的关系)。

// 会报错

let mut s = String::from(“hello”);

let r1 = &mut s;

let r2 = &mut s;

println!(“{}, {}”, r1, r2);

// 会报错 too

let mut s = String::from(“hello”);

let r1 = &s; // 没问题

let r2 = &s; // 没问题

let r3 = &mut s; // 大问题

println!(“{}, {}, and {}”, r1, r2, r3);

这个限制的好处是 Rust 可以在编译时就避免数据竞争,可以理解为类似于分布式锁的玩意儿~ 数据竞争会导致未定义行为,难以在运行时追踪,并且难以诊断和修复;Rust 避免了这种情况的发生,因为它甚至不会编译存在数据竞争的代码 注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用在声明可变引用之前,所以如下代码是可以编译的:

let mut s = String::from(“hello”);

let r1 = &s; // 没问题

let r2 = &s; // 没问题

println!(“{} and {}”, r1, r2);// 此位置之后 r1 和 r2 不再使用

let r3 = &mut s; // 没问题

println!(“{}”, r3);

但是虽然可以编译,这样书写是绕不过静态类型检查的!!!!!!

相信大家发现了上面的string类型有些特殊,不是说string是"值类型"吗?为什么他又可以用引用类型来表示呢?string使用了没有所有权的特殊的引用类型slice,slice 允许你引用集合中一段连续的元素序列,而不用引用整个集合。

let s = String::from(“hello”);

let slice = &s[0…2];

let slice = &s[…2];

对于"值类型"的string

let s = “Hello, world!”;

这里 s 的类型是 &str:它是一个指向二进制程序特定位置的 slice。这也就是为什么字符串字面值是不可变的;&str 是一个不可变引用。

悬垂引用 悬垂指针是其指向的内存可能已经被分配给其它持有者。相比之下,在 Rust 中编译器确保引用永远也不会变成悬垂状态 例如以下代码:

fn main() {

let reference_to_nothing = dangle();

}

// wrong

fn dangle() -> &String {

let s = String::from(“hello”);

&s

}

//safe

fn safe() -> String {

let s = String::from(“hello”);

s

}

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后,s 将被释放。不过我们尝试返回它的引用。这意味着这个引用会指向一个无效的 String,这可不对!Rust 不会允许我们这么做 总结两条规则

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。

  • 引用必须总是有效的。

到这里和JavaScript有联系的,并且基础的就分享的差不多了,隐约记得鲁迅说过,如果你对一门语言,了解了其基本的语法,能够编写对应的简单的代码来实现简单的功能,那么你就入门了。后续的包括以下部分,就先按下不表

  • Cargo : Cargo 是 Rust 的构建系统和包管理器。大多数 Rustacean 们使用 Cargo 来管理他们的 Rust 项目,因为它可以为你处理很多任务,比如构建代码、下载依赖库并编译这些库。类似于JS使用的npm/pnpm/yarn

  • 常见集合:Hashmap(类似于js中的map),Vector(类似于js中的数组),String

  • 错误处理:panic(Throw Error 完全阻塞了程序执行) Result(类似于warning 可以报错但是不影响程序的执行)

最后总结一下rust我认为最令人称道的两点

  1. 丰富而强大的类型系统

  2. 可信赖的所有权模型

Rust and WebAssembly

====================

上面讲了半天rust,他只是我们今天的猪脚之一,那么今天的猪脚还有哪位呢?没错,就是 WebAssembly。那么WebAssembly到底是什么呢?在说这个之前先康康JavaScript的是怎么进行编译的 这就不得不说到两种编译方式了

  • AOT: Ahead-of-Time compilation

必须是强类型语言,编译在执行之前,编译直接生成CPU能够执行的二进制文件,执行时CPU不需要做任何编译操作,直接执行,性能最佳,比如C/C++,Rust

  • JIT: Just-in-Time compilation

没有编译环节。执行时根据上下文生成二进制汇编代码,灌入CPU执行。JIT执行时,可以根据代码编译进行优化,代码运行时,不需要每次都翻译成二进制汇编代码,V8就是这样优化JavaScript性能的。

举个例子,如果使用var来声明一个变量,不使用Typescript等类型系统来限定,一个变量,在多次编译的时候得到的变量的类型可能会不一样,这就导致了每一次JavaScript在执行的时候可能都会被重新编译,这就是类型系统的重要性,不仅能减少bug的发生也可以让我们的代码跑得更快

详细的说一下这个过程也就是

  1. 代码文件会被下载下来。

  2. 然后进入Parser,Parser会把代码转化成AST(抽象语法树).

  3. 然后根据抽象语法树,Bytecode Compiler字节码编译器会生成引擎能够直接阅读、执行的字节码。

  4. 字节码进入翻译器,将字节码一行一行的翻译成效率十分高的Machine Code.

有同学可能会问:JavaScript不是可以使用Typescript进行静态类型检查吗?为什么不能在编译时编译成可执行的二进制文件呢?盲生,你发现了华点!Typescript说白了也只是给JavaScript打上了补丁,但是JavaScript还是那个JavaScript,说不定在有生之年可以看见JavaScript的整个内核被重写呢?Wasm:那我走?

回到正题,既然JavaScript的内核变化的几率不大,那我们该如何进行优化呢?一个思路就是可以直接把 C、C++、Rust等语言编译成 WebAssembly 并能在浏览器中运行,但是有一点需要注意,使用wasm并不是完全舍弃掉了JavaScript,这两者实际上是相辅相成的关系,在实际的应用场景中Rust和JavaScript往往是互相调用包来开发一个web应用。

WebAssembly是一份字节码标准,以字节码的形式依赖虚拟机在浏览器中运行。万维网联盟(W3C)2019年12月5日宣布,WebAssembly 核心规范 现在是一种正式的 Web 标准,它为 Web 发布了一种功能强大的新语言。WebAssembly 是一种安全、可移植的低级格式,能够在现代处理器(包括 Web 浏览器)中高效执行并紧凑地表示代码。它也被设计为可以与JavaScript共存,允许两者一起工作。这样说大家可能云里雾里的,那么换个方法 我们每天都在接触各种业务,那大家有没有想过从我们写下JavaScript代码开始,到底发生了什么?就只看JavaScript大致是这样一个过程:

业务代码 -> v8 解析 -> 得到编译结果(字节码) -> 线程通信 -> 通知GPU绘制 -> 渲染

那如果我们使用了WebAssembly,那又是一个什么过程呢?

业务代码 -> 编译 -> 字节码 -> 线程通信 -> 通知GPU绘制 -> 渲染

可以看出,这两个链路最大的区别就是,在第二种链路中,浏览器(V8)所得到的东西,已经是一份可以执行的字节码了,他只需要执行就完事了,而不需要使用大量的CPU来对可能很复杂的源代码来进行编译。(当然也可以使用worker 这里就不做讨论了) 但是纯纯的字节码指定是不行的,C/C++,Rust可能都有自己的一套规范,所以这就需要一套规范来整合一下,让大家都可以愉快的在浏览器中玩耍,这可以说就是WebAssembly,由他的标准可以生成后缀名为.wasm的文件,可以直接交给浏览器执行 目前主流的浏览器都已经支持了WebAssembly。除此之外 ,依照wasm的特性,个人认为或者wasm未来在多端也能有一定的用处

实战

俗话说的好,纸上得来终觉浅,绝知此事要躬行,上面简单学习了rust+wasm,那如果不实践一下那不是浪费了吗,那到底怎么实践rust+wasm呢?自己看着wasm的文档写?那指定是不行的。那怎么办呢?不要慌,今天的第三位猪脚出现了:Yew 文档在此yew中文文档简介如下Yew 是一个设计先进的 Rust 框架,目的是使用 WebAssembly 来创建多线程的前端 web 应用。

  • 基于组件的框架,可以轻松的创建交互式 UI。拥有 React 或 Elm 等框架经验的开发人员在使用 Yew 时会感到得心应手。

  • 高性能 ,前端开发者可以轻易的将工作分流至后端来减少 DOM API 的调用,从而达到异常出色的性能。

  • 支持与 JavaScript 交互 ,允许开发者使用 NPM 包,并与现有的 JavaScript 应用程序结合。

让一个yew应用跑起来分三步(确信)

  1. 创建一个二进制项目

cargo new --bin yew-app && cd yew-app

  1. 编写代码,注意要编写index.html

  2. 启动

cargo install trunk wasm-bindgen-cli

rustup target add wasm32-unknown-unknown

trunk serve

一张图简述一下wasm-bindgen的作用

组件化

页面展示的代码

use yew::prelude:声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】

推荐阅读
相关标签