初学rust,trait的一些使用限制
Tag rust, trait, on by view 32

rust中,trait相当于其他语言中的接口。例如json序列化,需要实现Serialize这个trait,但是,这里存在如下三个限制:

  1. 要实现的 trait 和类型都在当前 crate 中定义。
  2. 要实现的 trait 在当前 crate 中定义,而类型在外部 crate 中定义。
  3. 要实现的类型在当前 crate 中定义,而 trait 在外部 crate 中定义。

也就是说,你想要对一个结构体去实现某个trait,要么,这个结构体是你的(也就是在你当前crate包中),要么,这个trait是你定义的,两者至少有一个是你自己的,你才能对这个结构体实现这个指定的trait。

如果,trait和结构体都不是你实现的,你将无法为这个外部结构体实现外部trait。例如,我想为TcpStream实现Serialize,直接写,会报错

sfgdut7l

`mio::net::TcpStream` is not defined in the current craterustcE0117
main.rs(24, 1): original diagnostic

只能通过包裹TcpStream来实现了,总之,为外部结构体实现外部trait就是行不通。


初学rust,rust数字类型转换不安全
Tag rust, 类型转换, on by view 53

数字类型转换是所有语言中存在的一项操作。比如golang的float64转float32,用float32(v64)即可实现,但是这个转换在v64的值只要不超过float32的上限,就可以安全转换。但是,这在rust中似乎行不通。

我在一个实践程序里在结构体中定义了一系列的f32,i32字段,然后用parse::<f32>()从字符串解析出f32,这一切都正常,我单独单测代码打印过这个结果,正确的读取了数字;但是当我将其进行了一系列的累加之后,转为f64再json序列化,发现居然变成了null,然后我debug打印发现,数字变成inf了,也就是f64::INFINITY,我类型转换过程中用的就是as f64

后来我索性将定义的f32改为了f64,最后它又正常了。这说明as操作并没有想象中那么安全。


初学rust,多线程中读写全局变量
Tag 全局变量, 读写, rust, on by view 101

参考上一篇《初学rust,如何实现在运行时对全局变量设置和读取》,文章中说,可以用OnceCell来定义全局变量,然后就可以对全局变量进行读写,实际上,我发现OnceCellset()方法只能调用一次,如果你试图第二次调用set()来修改已经设定好的值,将会报错,设置值就失败了。

由于这一切都是为了读写的并发安全。所以OnceCell是无法实现读写全局变量的。要实现读写全局变量,应该使用RwLock。如下

定义全局变量

static WORKER_PID: RwLock<Option<u32>> = RwLock::new(None);

读写全局变量

// 写入
fn store_worker_pid(pid: u32) {
    let mut data = WORKER_PID.write().unwrap();
    *data = Some(pid);
}

// 读取
let data = WORKER_PID.read().unwrap();
let cpid = data.unwrap();

这里的读写就可以多次进行了。可以看到,这里使用了读写锁,写的时候会先通过write()方法获取写入锁实例,然后对其赋值写入。读取的时候会先通过read()方法获取读取锁实例,然后取里面的值。这样一来就实现了全局变量的读写。


初学rust,不允许遍历过程中修改HashMap
Tag rust, HashMap, 遍历, 修改, on by view 76

先看一段代码

#[test]
fn test_hash_map() {
    let mut mp: HashMap<&str, &str> = HashMap::new();
    mp.insert("k", "v");
    mp.insert("k1", "v1");
    let x = mp.keys().clone();
    for k in x {
        mp.insert("k2", "v1");
        mp.insert("k3", "v1");
    }
    mp.insert("k2", "v1");
    mp.insert("k3", "v1");
}

看,它报错

error[E0502]: cannot borrow `mp` as mutable because it is also borrowed as immutable
   --> src/process.rs:153:9
    |
151 |     let x = mp.keys().clone();
    |             -- immutable borrow occurs here
152 |     for k in x {
    |              - immutable borrow later used here
153 |         mp.insert("k2", "v1");
    |         ^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

报错告诉我们,不允许将mp作为mutable,因为它已经用于immutable了。难道,HashMap插入数据完毕,开始读取数据之后,不能再次插入数据了?我一开始这么怀疑,不应该啊,于是代码改成这样

#[test]
fn test_hash_map() {
    let mut mp: HashMap<&str, &str> = HashMap::new();
    mp.insert("k", "v");
    mp.insert("k1", "v1");
    mp.keys().clone();
    mp.insert("k2", "v1");
    mp.insert("k3", "v1");
}

正常了,没报错了。看样子问题出在这个for ..in..中。仔细分析代码,我在for中遍历了它的key,或者说我正在将这个HashMap中的数据拿出来,这时候,我在for中尝试往这个HashMap中写入数据,写入数据会让这个HashMap发生变更,这里第一感让我觉得可能有问题。比如我HashMap中有10个元素,我在遍历它,然后在for中间插入新元素,那么是不是有下列问题:1,我的HashMap会不会越遍历越多,会不会永远无法遍历完;2,HashMap是无序的,我将新元素插入HashMap中,会不会导致我已经遍历过的数据由于插入新数据,导致再次被读取出来,因为它可能位置发生变化了嘛。

其实这就是数据竞争和它带来的不确定性问题,rust作为一个内存安全第一的编程语言,编译器会教你做人。

于是,我再改

#[test]
fn test_hash_map() {
    let mut mp: HashMap<&str, &str> = HashMap::new();
    mp.insert("k", "v");
    mp.insert("k1", "v1");
    let x: Vec<&str> = mp.keys().map(|k| *k).collect();
    for k in x {
        mp.insert("k2", "v1");
        mp.insert("k3", "v1");
    }
    mp.insert("k2", "v1");
    mp.insert("k3", "v1");
}

这回正常了。可以看到我的操作let x: Vec<&str> = mp.keys().map(|k| *k).collect();是将keys()拿到的Keys迭代器(仍旧从前面的HashMap里迭代)通过.collect()方法将迭代器里的元素“倒”入到Vec<&str>,这样这个Vec就是一个独立与HashMap内存空间之外的变量,再基于这个Vec进行遍历,就可以避免“边遍历边修改的”的情况了。

那么大家可以思考一下,其他语言,比如golang,遇到这种情况是怎么处理的呢。


初学rust,踩坑私有依赖
Tag rust, 依赖, git, on by view 164

最近尝试在一个项目中引用一个私有crate,这个crate是一个lib,然后存储在私有git仓库中,并且这个git平台不支持rust的crate-index.

  • 第一坑,引用git依赖

于是我在我的项目中这样引用外部依赖包

[package]
name = "las"
version = "0.1.1"
authors = ["...."]
edition = "2018"

[dependencies]
elasticsearch = { version = "8.5.0-alpha.1", default-features = false, features = [
    "rustls-tls",
] }
logwatcher = "0.1.1"
json = "*"
reqwest = { version = "0.11.12", default-features = false, features = [
    "blocking",
    "rustls-tls",
] }
toml = "0.8.6"
serde_derive = "1.0.177"
serde = "1.0.177"
serde_json = "1.0.104"
once_cell = "1.18.0"
futures = "0.3"
tokio = { version = "*", features = ["full"] }
uuid = { version = "1.4", features = ["v4", "fast-rng", "macro-diagnostics"] }
chrono = "0.4"
core_affinity = "0.8.0"
geoip = { git = "ssh://git@git.xxxx.com/xxxx/tgeoip.git", branch = "master", package = "geoip" }

nix = "0.26"
libc = "0.2.146"
clap = { version = "4.4.2", features = ["derive"] }
log4rs = { version = "1.2.0" }
log = { version = "0.4.20" }
[build-dependencies]
regex = "1.6.0"

需要注意的是,我git@git.xxxx.com:xxxx/tgeoip.git仓库中是一个workspace,里面有2个子项,一个是可执行bin项目,叫做regen,一个是外部可依赖包,叫做geoip(后续实践表明同一个workspace里面的子项引用就应该这么干,git路径是同一个,使用package指定子项的包名),我这样引入之后,发现死活拉取不了私有git依赖。报错没权限拉取,git鉴权失败

error: failed to get `geoip` as a dependency of package `las v0.1.1 (/data/code/rust/las)`

Caused by:
  failed to load source for dependency `geoip`

Caused by:
  Unable to update ssh://git@git.xxxx.com/xxxx/tgeoip.git?branch=master#e41c5279

Caused by:
  failed to clone into: /root/.cargo/git/db/tgeoip-9094aceea5940357

Caused by:
  failed to authenticate when downloading repository

  * attempted ssh-agent authentication, but no usernames succeeded: `git`

  if the git CLI succeeds then `net.git-fetch-with-cli` may help here
  https://doc.rust-lang.org/cargo/reference/config.html#netgit-fetch-with-cli

Caused by:
  no authentication methods succeeded

各方查询后,都是建议添加这个选项,我开始还没搞清楚加在哪儿,但是发现使用环境变量好使。各方尝试后:

添加到~/.cargo/config文件中,如下

[net]
git-fetch-with-cli = true

然后再次执行cargo build无需环境变量即可。(前提是你本地已经配置好了访问ssh://git@git.xxxx.com/xxxx/tgeoip.git的ssh key)

  • 第二坑,外部依赖包要申明为rlib

我在这个bin项目中引用geoip,发现一直报错

failed to resolve: use of undeclared crate or module `geoip`

百思不得其解,最后发现是我依赖包申明类型的问题,我Cargo.toml申明的是

[package]
....

[lib]
name = "geoip"
crate-type = ["staticlib", "cdylib"]

[dependencies]
bincode = "1.3.3"
serde = { version = "1.0.193", features = ["derive"] }

实际lib应该申明rlib类型,才能被正常引用,改为如下,更新依赖,就没报错了

[package]
....

[lib]
name = "geoip"
crate-type = ["staticlib", "cdylib", "rlib"]

[dependencies]
bincode = "1.3.3"
serde = { version = "1.0.193", features = ["derive"] }

初试riscv机器
Tag riscv64, linux, on by view 31

最近入手了一块riscv64的开发板,是某宝上入手的StarFive2,准备拿它当做riscv64架构的服务器用来编译程序。开始刷上的是官方给的debian系统,成功启动了,nvme正常,但是有两个问题,第一个是apt update会报签名错误,第二个是不支持docker,想要让它支持docker,需要自己编译升级内核,非常麻烦。于是决定换用ubuntu版本的镜像。

根据官方教程,需要先将板载固件刷新到ubuntu社区的指定版本,从这里下载,需要注意的是,目前(2023/12/18)最新版的固件(u-boot-starfive-2024.01~rc4-0ubuntu1~ppa1这个)会导致kernel panic,需要下载下面的版本

2flhl8vj,展开后,用wget下载deb安装包,找个ubuntu机器,用dpkg -x解包,将里面的下面两个文件上传到sdcard.img刷机包启动的系统里面

g80xfqba

更新SPL

flashcp -v u-boot-spl.bin.normal.out /dev/mtd0

更新U-Boot

flashcp -v u-boot.itb /dev/mtd2

然后安装ubuntu系统,注意从这里选择下面版本的系统

jerua3mn

使用etcher将固件写入到sd卡,插入sd卡,启动,正常情况就可以启动成功。

在这中间我踩过一些坑,在此也一起分享一下。首先是hdmi插在4k屏无法显示,这样一来,我就不知道当前系统启动得怎么样了,因此kernel panic了,我也不知道,后来只能使用usb转串口用电脑连接串口,将串口控制台当做显示器用,才知道启动过程中发生了什么,串口连接如下图

3atnih7j

从串口控制台看到如下图(学到了,串口还能当显示器用)

y2m6970v

可以看到,启动过程卡死在kernel panic,我这才知道是内核崩溃了。看起来是u-boot-starfive-2024.01~rc4-0ubuntu1~ppa1这个版本的固件,对现有的23.04版本的系统兼容性有问题,所以我换了上个版本的固件,成功启动。

到此,在StarFive2上成功的安装上了ubuntu系统,docker也成功的安装,不过这个版本的板载固件 有个问题,就是无法识别nvme硬盘,希望下个正常版本能够支持。


初学rust,错误处理
Tag 错误处理, rust, on by view 37

rust中如何比较优雅的进行错误处理,这是一直以来困扰我的一个问题。最近写了一个ip地址库查询包,于是在其中实践了一下自定义错误、错误抛出等处理。

rust中,如果一个函数需要返回错误,那么应该用Result包裹返回值,Result定义如下

enum Result<T, E> {
    Ok(T),
    Err(E),
}

通常,Result中的第二个参数就是错误。比如Result<String, String>,那么它的错误就是一个String类型的值,如下

// 返回了正常值hi
fn find() -> Result<String, String> {
    Ok("hi".to_string())
}

// 返回了错误信息error
fn find() -> Result<String, String> {
    Err("error".to_string())
}

那么有另外一个问题,就是,我们如何处理其他库或者第三方函数抛出给我们的错误呢?我们可以用expect(),unwrap()来解决可以解决的错误,但是有时候我们不希望处理错误,希望能够将错误抛出给上层,让上层调用者去处理。这时候,我们应该怎么定义错误类型?你当然可以在你的函数中进行错误处理之后,抛出String类型的错误,以便继续使用String类型作为错误类型。

#[test]
fn test_find() {
    let x = foo();
    println!("{:?}", x.unwrap())
}

fn foo() -> Result<File, String> {
    let x = bar();
    match x {
        Err(e) => Err(e.to_string()),
        Ok(f) => Ok(f),
    }
    ...
}

fn bar() -> Result<File, io::Error> {
    File::open("regions.txt")
}

你可以看到,我在foo()中调用bar()bar()中返回了一个包含io::Error的错误,而我的foo()中要求返回的是String,我在foo中用match处理了错误的情况,并且foo中可能调用其他第三方库函数,返回的错误类型不尽相同,我每种错误类型都可以使用match解开,然后返回String类型的错误。但是这样处理起来,代码看起来就非常乱,到处都是match错误。

其实我们有另一种方法,可以自己定义一个Error类型,然后对Error类型进行扩展,让它兼容其他类型,如下

use std::{fmt, io};

#[derive(Debug)]
pub enum Error {
    ParseError,
    ReadError,
    InvalidIPError,
}

impl std::error::Error for Error {}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::ParseError => write!(f, "Parse Error"),
            Error::ReadError => write!(f, "Read Error"),
            Error::InvalidIPError => write!(f, "Invalid IP Error"),
        }
    }
}

impl From<io::Error> for Error {
    fn from(_: io::Error) -> Self {
        Error::ReadError
    }
}

impl From<bincode::Error> for Error {
    fn from(_: bincode::Error) -> Self {
        Error::ParseError
    }
}

impl From<std::net::AddrParseError> for Error {
    fn from(_: std::net::AddrParseError) -> Self {
        Error::InvalidIPError
    }
}

我们的Error类型,实现了fmt::Display,让它能够处理我的包项目中可能出现的第三方错误,并且针对这些错误实现了各自的From<T>from方法,这样,这些第三方错误就可以直接以我的Error返回,使用?简写之后,代码就改成这样了

#[test]
fn test_find() {
    let x = foo();
    println!("{:?}", x.unwrap())
}

fn foo() -> Result<File, Error> {
    let x = bar()?;
    Ok(x)
}

fn bar() -> Result<File, io::Error> {
    File::open("regions.txt")
}

对比可以看到,我在foo中处理bar抛出的错误变得简单了,直接一个问号就将可能得错误抛出到Error了,然后再将Ok(x)返回。这样一来,错误的抛出和处理就变得优雅多了。


gin.Context 异步调用踩坑
Tag gin, context, 异步, goroutine, on by view 159

事情的背景是这样了,我在gin框架的action中有个异步调用逻辑,然后异步调用需要使用context.Context接口作为参数传入,异步调用的模块中会从context中取request_id作为追踪追踪标记。于是我就直接讲*gin.Context作为参数传入了异步调用中。

然后灾难就发生了,我日志记录中能查到当前请求的request_id,但是发现条数不对,异步请求中记录的日志条数有10条,我用request_id去搜索,只查到了4条,最后我只能添加另外标记,通过另外的标记查询到10条日志,让我奇怪的是,另外6条日志的request_id却不是当前请求的request_id。

最后,我发现gin.Context专门有个Copy方法,是将context拷贝一份,我调用Copy拷贝一份context之后将拷贝的context传入异步调用,果然10条日志的request_id一致了。看来,从gin框架传入进来的context在不同的action中是复用的。然后在请求处理完毕之后,会给其他请求复用,这样传给异步调用模块的context中的request_id已经变了。

r.GET("/async", func(c *gin.Context) {
	// 需要搞一个副本
	copyContext := c.Copy()
	// 异步处理
	go func() {
		time.Sleep(3 * time.Second)
		log.Println("异步执行:" + copyContext.Request.URL.Path)
	}()
})

这里必须要谨记,如果需要在action中异步调用并使用context传参,必须要将context.Copy()之后再传入。否则context就会被其他协程修改。


Bazel对依赖库进行patch遇到的问题
Tag bazel, patch, 依赖, on by view 149

最近使用bazel需要对一个go第三方依赖进行patch,这个依赖是gorm/v1,原作者对v1已经放弃了,我们项目中使用了v1,但是因为v1到v2是不兼容的升级,所以我们项目中无法升级。针对v1的优化和bug修复,就只能使用patch的方式进行了。

第一步将github.com/jinzhu/gorm克隆下来,check出最后一个版本v1.9.16,然后做修改。修改完毕之后,不要进行git提交,执行git diff >> new1.patch将会生成这次变更的patch文件。

将patch文件存储如下0001-go-errors.patch

third_party_patches
└── com_github_jinzhu_gorm
    ├── 0001-go-errors.patch
    ├── 0002-fix-sqlite.patch
    └── BUILD.bazel

其中,BUILD.bazel空文件即可。另外deps.bzl中的引用如下

go_repository(
    name = "com_github_jinzhu_gorm",
    build_file_proto_mode = "disable",
    importpath = "github.com/jinzhu/gorm",
    patch_args = ["-p1"],
    patches = [
        "//third_party_patches/com_github_jinzhu_gorm:0001-go-errors.patch",
        "//third_party_patches/com_github_jinzhu_gorm:0002-fix-sqlite.patch",
    ],
    sum = "h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=",
    version = "v1.9.16",
)

但是我编译的时候,却报错了

➜  code git:(feature/migrate-flow) ✗ bazel build //...
INFO: Analyzed 74 targets (3 packages loaded, 141 targets configured).
INFO: Found 74 targets...
ERROR: /root/.cache/bazel/_bazel_root/aa05a35ce6b6cd2ce6b5a504fd9d9e22/external/com_github_jinzhu_gorm/BUILD.bazel:3:11: GoCompilePkg external/com_github_jinzhu_gorm/gorm.a failed: (Exit 1): builder failed: error executing command bazel-out/host/bin/external/go_sdk/builder compilepkg -sdk external/go_sdk -installsuffix linux_amd64 -tags sqlite -src external/com_github_jinzhu_gorm/association.go -src ... (remaining 71 argument(s) skipped)

Use --sandbox_debug to see verbose messages from the sandbox
compilepkg: missing strict dependencies:
        /root/.cache/bazel/_bazel_root/aa05a35ce6b6cd2ce6b5a504fd9d9e22/sandbox/linux-sandbox/4877/execroot/ias-admin/external/com_github_jinzhu_gorm/main.go: import of "github.com/go-errors/errors"
No dependencies were provided.
Check that imports in Go sources match importpath attributes in deps.
INFO: Elapsed time: 0.843s, Critical Path: 0.06s
INFO: 4 processes: 4 internal.
FAILED: Build did NOT complete successfully

按照报错打开bazel cache路径,发现bazel为依赖生成的BUILD.bazel文件中,并没有添加我patch中引入的依赖。于是我将cache中的BUILD.bazel拷贝出来,手动加入依赖,然后git diff,得到BUILD.bazel的patch文件如下


diff --git a/BUILD.bazel b/BUILD.bazel
index 0849476..d26e9f7 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -32,7 +32,10 @@ go_library(
     ],
     importpath = "github.com/jinzhu/gorm",
     visibility = ["//visibility:public"],
-    deps = ["@com_github_jinzhu_inflection//:inflection"],
+    deps = [
+        "@com_github_jinzhu_inflection//:inflection",
+        "@com_github_go_errors_errors//:go_default_library",
+    ],
 )
 
 alias(
@@ -76,5 +79,6 @@ go_test(
         "//dialects/sqlite",
         "@com_github_erikstmartin_go_testdb//:go-testdb",
         "@com_github_jinzhu_now//:now",
+        "@com_github_go_errors_errors//:go_default_library",
     ],
 )

加入到我们的patch中,然后再次编译,就正常了。

我推断bazel patch这里面的逻辑是,先拉下原始包,第二步生成BUILD.bazel,最后再patch。google的研发bazel的人简直就是没长脑子,这样的步骤我还要它生成BUILD.bazel干啥,还不如直接把BUILD.bazel生成到patch文件里。总之,折腾了许久总算解决了,记录一下。


初识串口
Tag 串口, on by view 40

一直很想了解设备驱动编程。所以就从最基础,最简单的设备开始了解。串口,应该是所有设备中最简单最常见的设备。在linux中,有一种最简单的串口设备在/dev下表现的是字符设备(c),字符设备可以被打开并且读写。

我认识的第一个最简单的串口设备是一个usb接口的gps模块,如下图 fmvi8se8 它是我目前为止,用过的最简单的串口设备,它只需要读,无需写,单向通讯,我只需要打开字符设备,然后不停的从中读取数据就可以了,里面的数据是GPS定位信息相关的字符串,根据它的文档,进行字段解析,就能获取到经纬度、时间、角度、速度等信息。我曾经用golang写过一个一个程序获取这些信息 sbtao9zf 这个程序,实际上可以算是一个用户态的设备驱动程序。

我认识的第二个串口设备是一套蓝牙模块,用了usb转串口线连接蓝牙模块 ddlsq0na 按照设备的文档,我将它通过usb转串口线连接,然后将usb插上电脑,成功识别到串口。然后手机通过客服提供的调试app连接蓝牙信号,电脑上用串口读写软件将串口打开,手机上发送信息,电脑串口上能收到信息,电脑串口上还能发送特定的AT指令对设备进行查询与设置。 3wwg8uas 值得注意的是,我在连接模块的过程中,将usb转串口上的tx连接了模块tx,rx连接了模块的rx,导致无法收发消息,客服帮我找出问题并告诉我应该将tx连接rx,rx连接tx。想到这一点我恍然大悟,这跟rust里的channel,go里的chan,以及linux上的通用管道一样,都属于管道模型,对端的输出接入到设备的输入,对端才能将信息传给设备。后续我想我会进一步对这个蓝牙模块编写用户态驱动程序,进而尝试编写内核态驱动程序。

其实早在我认识GPS串口设备之前,我也试过温湿度传感器DHT22,但是,这玩意儿根本不是串口,它只有一个数据接口(DATA),也就是不像串口那样分rx,tx,它需要通过gpio口连接DATA针,然后通过高电位低电位来实现双向通讯,从而获取到温湿度数据。它既然不是串口设备那么也不能作为通用设备直插一般的电脑了。

在我看来,程序能驱动物理设备,也是一件很有趣的事情。