#nodejs #events #loops #js #node #scope #environment

noders

Rust 的类似 NodeJS 的事件循环环境

3 个版本

使用旧的 Rust 2015

0.0.2 2018 年 9 月 25 日
0.0.1 2018 年 9 月 24 日
0.0.0 2018 年 9 月 24 日

1179Rust 模式

MIT 许可证

45KB
939 行 代码

node.rs

Rust 的类似 NodeJS 的框架。

使 NodeJS 编程变得愉快的主要原因是其 API。即使是 JavaScript 的忠实粉丝也会同意,JavaScript 有一些会引起混乱的设计决策,但 JavaScript,特别是 NodeJS 的重要之处在于,通常编写在其上的代码通常“Just Works”。没有复杂的类型系统(或任何类型系统)来阻碍快速开发,API 在让人们做典型事情变得简单和避免“魔法”命令之间取得了良好的平衡,这些命令难以理解,难以推理,或者更糟糕的是,API 设计者期望消费者盲目调用。

为什么 API 很重要

以下是 API 为什么如此重要的例子。这是一个读取文件并将所有“cloud”(不区分大小写)单词替换为“butt”单词的小程序。我们的第一个示例展示了 Java 标准库的糟糕 API

import java.util.regex.Pattern;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ApiMatters
{
    static final Pattern REGEX = Pattern.compile("cloud", Pattern.CASE_INSENSITIVE);
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader("file.txt"));
        try {
            String line = br.readLine();
            while (line != null) {
                System.out.println(REGEX.matcher(line).replaceAll("butt"));
                line = br.readLine();
            }
        } finally {
            br.close();
        }
    }
}

Java 希望你知道 如何 进行操作,每个细节都是必要的,你需要知道你正在读取一个 File,使用 FileReader,如果你可能不想每次读取时都调用 syscall,那么你需要一个 BufferedReader。而进行正则表达式替换需要创建一个 Pattern 对象,你应该将其作为静态字段放在你的 Object 类中,你正在创建一个 Object,对吧?

在另一个极端是魔法,我能想到的最好的例子是旧的 Bitcoin 代码库,当数据结构需要从磁盘或网络读取或写入时,它们需要转换为序列化形式。Satoshi 对于此的可爱解决方案是有一个魔法宏:IMPLEMENT_SERIALIZE,它扩展到大约 1000 行嵌套的 C++ 宏代码。Satoshi 值得赞扬的是,他只在自己的代码中使用这个怪物。真正的罪行是将这些 魔法结构 作为 API 导出。

魔法的API编写者不会试图解释正在发生的事情,相反,他们编写的文档实际上就是“直接调用,不要问太多问题,它会工作的”。但如果它不工作,你将陷入痛苦的世界,试图理解那千行元元元多态编程到底做了什么。魔法API的基本问题在于它们缺乏一个稳固的隐喻,你真的不知道打开灯是否会导致马桶冲水,如果它确实这样做了,那么这到底是bug,还是一些鲁比·戈德堡式的“特性”,以拯救疯狂的API作者。

Node.js易于使用,因为API创建者采取了中间道路。JavaScript(截至2018年)没有强大的宏,因此创建魔法API并不容易,而Node.js的创建者做出了坚实的努力,避免了全局标志副作用,同时在API设计中隐藏了大多数典型程序员不太关心的东西。

以这个片段为例

const Fs = require('fs');
Fs.readFile('./file.txt', 'utf8', (err, data) => {
    if (err) { throw err; }
    data.split('\n').forEach((l) => {
        console.log(l.replace(/cloud/i, 'butt'));
    })
});

就像那个大的Java示例一样,它也读取文件,将“云”替换为“屁股”,并写出结果。与大的Java示例不同的是,它不需要程序员了解缓冲区、正则表达式编译或在整个过程中可能发生的许多类型的错误。一个公平的批评是,Node.js不容易验证是否已处理所有异常情况,但在你试图让项目起飞时,处理所有异常情况是最不重要的事情。

什么是Rust

Rust是一种编译型语言,所以就像C/C++一样,它可以创建小的快速独立二进制文件。Rust也是一种内存安全的语言,所以就像JavaScript一样,它不能发生段错误*。Rust的类型系统是一个先进的系统,与函数语言如Haskell相似,但Rust本身是过程性的,通常类似于C++。在某种程度上,你可以想象Rust是两种语言,你用过程性语言编写代码,用函数性语言说服类型系统你的代码是安全的。

介绍node.rs

node.rs是试图将Node.js的好东西带到Rust中的一种尝试。它建立在MIO之上,并包含一个嵌入式事件循环和回调功能。

简单示例

最简单的例子是setTimeout(),与Node.js不同,你需要显式启动事件循环,你可以通过模块构建器做到这一点。构建模块后,你会接到一个范围。范围提供了对底层事件循环的访问,并且是传递给每个回调的第一个参数。我们稍后会详细介绍模块构建器,但现在你可以用以下方式将程序包装起来module().run((), |s| { …… });

extern crate noders;
fn main() {
    noders::module().run((), |s| {
        noders::time::set_timeout(s, |s, _| {
            println!("Hello1");
        }, 100);
    });
}

范围

在JavaScript和其他类似于Scheme的语言中,嵌套范围可以像这样访问和修改父范围的变量

let sum = 0;
[1,2,3,4,5].forEach((i) => {
    sum += i
});
console.log(sum);

这个例子相当简单,因为所有事情都是同步发生的。在代码片段执行完毕之前,数字 sum 就已经消失了。

然而,在这个例子中

let x = 0;
setTimeout(() => { x++; }, 100);
setTimeout(() => { console.log(x); }, 200);

数字 x 需要在声明它的函数返回之后继续存在。JavaScript 通过单线程执行和垃圾回收来实现这一点,因为有两个闭包被注册到了 setTimeout 函数上,并且这两个闭包持有 x 的引用,JavaScript 将会在它们完成之前保留 x 的内存位置。

由于 Rust 没有垃圾回收器,内存中的每个对象都必须有一个唯一的 所有者,此外,为了避免 指针别名 问题,Rust 语言的规则指定,如果有指向一个对象的可变指针,那么在同一时间不能有指向同一对象的任何其他指针。

因此,在 Rust 中,第一个例子是可行的

fn main() {
    let mut sum = 0;
    vec![1,2,3,4,5].iter().for_each(|i|{
        sum += i
    });
    println!("{}", sum);
}

但是第二个例子失败了,因为 i 是由主函数 所有 的,所以在主函数执行完毕时它会被释放。

// error[E0597]: `i` does not live long enough
fn main() {
    let fake_event_loop = || {
        let mut i = 0;
        return (
            || { i += 1; },
            || { println!("{}", i); }
        );
    };
    let mut callbacks = fake_event_loop();
    (callbacks.0)();
    (callbacks.1)();
}

在 Rust Playground 中尝试一下

作用域的工作方式

当你使用 module().run((), |s| { …… }); 创建模块时,你可能注意到 run() 的第一个参数是 (),即单元(类似于其他编程语言中的 null)。当你调用 run() 时,你可以传入任何对象,这个对象将被 包装 来创建一个作用域,所以使用以下模式,你可以获得一个工作中的作用域

extern crate noders;

struct Context {
    integer: i32,
    hi: &'static str,
    number: f32
}

fn main() {
    let ctx = Context {
        integer: 1,
        hi: &"Hello world",
        number: 3.5
    };

    noders::module().run(ctx, |s| {
        noders::time::set_timeout(s, |s,_| {
            println!("{} {}", s.hi, s.number);
            s.integer += 1;
        }, 100);
        noders::time::set_timeout(s, |s,_| {
            println!("{}", s.integer);
        }, 200);
    });
}

但是,请注意,作用域包装器添加了以下函数 cb()as_rc()core()。因此,如果你传入一个具有 core() 方法(例如)的对象,调用 core() 方法不会产生你预期的结果。

rec!{} 宏

由于使用 noders 会导致创建大量的临时作用域对象,存在 rec!{} 宏是为了帮助你快速创建匿名对象。由于 Rust 具有强大的类型推断,你通常只需传递值,Rust 就会检测到类型。所以这个

extern crate noders;
struct Context {
    integer: i32,
    hi: &'static str,
    number: f32
}
fn main() {
    noders::module().run(Context {
        integer: 1,
        hi: &"Hello world",
        number: 3.5
    }, |s| {
        noders::time::set_timeout(s, |s,_| {
            println!("{} {}", s.hi, s.number);
            s.integer += 1;
        }, 100);
        noders::time::set_timeout(s, |s,_| {
            println!("{}", s.integer);
        }, 200);
    });
}

可以简化为这个

#[macro_use(rec)] extern crate noders;
fn main() {
    noders::module().run(rec!{
        integer: 1,
        hi: &"Hello world",
        number: 3.5
    }, |s| {
        noders::time::set_timeout(s, |s,_| {
            println!("{} {}", s.hi, s.number);
            s.integer += 1;
        }, 100);
        noders::time::set_timeout(s, |s,_| {
            println!("{}", s.integer);
        }, 200);
    });
}

内部,rec!{}的作用是检查你创建的条目,并生成如下所示的代码:

{
    struct Rec<A,B,C> { integer: A hi: B, number: C }
    Rec { integer: 1, hi: &"Hello world", number: 3.5 }
}

Rust 的强类型推断系统能够确定对象的类型。注意:如果你创建了一个包含模糊值(例如 None)的 rec!{},Rust 可能无法检测到类型,你可能需要使用显式的结构。

在本文档中,我们将使用 rec!{} 宏来简化示例。

时间模块

任何基于事件系统的核心是能够安排在未来的某个时刻触发回调的方式。函数 set_timeout()set_interval()clear_timeout()clear_interval() 是时间模块的一部分。与它们的 JavaScript 同族一样,函数 set_timeout()set_interval() 返回一个 Token,可以与 clear_timeout()clear_interval() 一起使用,以取消超时或间隔。

extern crate noders;
use noders::time;
fn main() {
    let x = 3;
    noders::module().run(rec!{
        to: noders::Token(0)
    }, |s| {
        s.to = time::set_timeout(s, |_,_| {
            println!("This should never happen");
        }, 100);
        time::set_timeout(s, |_,_| {
            time::clear_timeout(s, s.to);
        }, 50);
    });
}

rec!{} 宏

创建子作用域

依赖关系

~1–1.3MB
~21K SLoC