Rust.cchttps://rust.ccThis Is Rust Crustacean Community RSS feed.一个axum的generate模板https://rustcc.cn/article?id=384a2568-1e33-4b5b-a871-ddc9b00a887d熟悉了下cargo generate的使用

]]>
2024-03-19 03:09:03
rust里如何实现C语言里的 void *context 功能? https://rustcc.cn/article?id=3a4cdb60-8db4-476e-9cff-655d47695730譬如,在 nginx 的C源码里,下面的 void *ctx 代码非常常见。 因为 context的类型可能千奇百怪,无法预测,只能用一个 void *来表示。 linux kernel 里也是大量的类似代码。 请问: rust里该如何正确的做呢?

struct ngx_module_s {
    ngx_uint_t            ctx_index;
    ngx_uint_t            index;
    char                 *name;
    ngx_uint_t            spare0;
    ngx_uint_t            spare1;
    ngx_uint_t            version;
    const char           *signature;
    void                 *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;
}
]]>
2024-03-18 22:27:35
用rust做透视自瞄的hack效果蛮可https://rustcc.cn/article?id=f23265da-89f0-48b0-9b3a-bd40363dc241老游戏,消逝的光芒,骨骼绘制+自瞄

用rust重写的cpp作者的代码

https://github.com/vSylva/dying_light_hack

自瞄有bug,哈哈哈,萌新不太会

]]>
2024-03-18 13:31:49
请教一个kube模块的使用问题https://rustcc.cn/article?id=750493b1-8eb8-47bf-86cb-4cf9cc7f1936#[tokio::test] async fn pod_test1() { let a = containers_info::<apps_v1::Deployment>().await.unwrap(); }

async fn containers_info<T>() -> Result<(), Box<dyn std::error::Error>> 
where 
T: Resource + DeserializeOwned + Debug + Clone,
T::DynamicType: Default,
{
    let kube_client = get_kube_client().await.unwrap();
    let kube_items: Api<T> = Api::all(kube_client);
    let lp = ListParams::default().fields(*SYSTEM_NAMESPACES);
    for item in kube_items.list(&lp).await? {
        let namespace = item.meta().namespace.as_ref().unwrap();
        let name = item.meta().name.as_ref().unwrap();            
        let container_type = T::kind(&T::DynamicType::default()).to_string();
        let spec = item.spec();
    }
    Ok(()) 
}

使用泛型的方法是为了减少重复代码,但是貌似不能输出spec中的字段, 但如果指定Api<apps_v1::Deployment> = Api::all(kube_client),则没有这个问题,请教大佬怎么解决

]]>
2024-03-18 10:45:06
【Rust日报】2024-03-18 Rust编程中的常见陷阱https://rustcc.cn/article?id=6cfa4e59-5618-4432-980f-1b488713053cRust编程中的常见陷阱

在Rust编程中,有些常见的陷阱需要我们注意避免:

  1. 所有权和借用:Rust独特的所有权系统强大但初学者容易迷惑。不当管理所有权和借用会导致复杂的编译错误。记住,在Rust中,每个值有唯一的所有者,正确处理所有权转移至关重要。
  2. 生命周期标注:生命周期确保借用的数据在使用期间有效,忽略或误解生命周期将导致晦涩的编译错误。需投入时间理解并正确使用生命周期。
  3. 可变别名:Rust的借用规则禁止多个可变引用指向同一数据,尝试这样做会导致编译器错误。如果你习惯了其他语言允许这种操作,这可能会让你措手不及。
  4. 对于unsafe的恐惧:使用unsafe代码时必须慎重,错误的使用可能会引入隐蔽的bug,破坏Rust的安全保障。每次考虑使用unsafe时,都应该三思是否有更安全的替代方案。
  5. 并发问题:Rust的并发模型以所有权和借用为基础,虽然在编译时防止了许多并发错误,但还是需要谨慎处理线程间的数据共享。忘记同步对共享数据的访问会导致数据竞争和漫长的调试过程。
  6. 字符串处理:Rust的字符串处理相对独特,特别是对于来自其他语言背景的人而言。理解String和&str间的差异,掌握字符串的所有权和借用,以及UTF-8编码相关的挑战都是必需的。
  7. 过度设计:Rust功能丰富,但并不意味着每个项目都需要使用其所有高级特性。代码过度设计会使得代码更难理解和维护。保持简单,根据具体情况使用合适的功能,避免过度展示你的Rust技能。

1.17.0 pre-relase已经可以测试

1.77.0 预发行版已准备好进行测试。该版本计划于 3 月 21 日发布。

用户可以通过下面来本地进行测试

RUSTUP_DIST_SERVER=https://dev-static.rust-lang.org rustup update stable

原文链接

hnsw-rust: HNSW的超快Rust实现

hnsw-rust 是分层 可导航小世界 (HNSW) 算法的 Rust 实现。HNSW是高维空间中近似最近邻(ANN)搜索的显着进步,从根本上改变了我们解决这些问题的方法。该算法构建了一个分层图结构,其中较高层(密度较低)用于快速全局导航,而较低层(密度较高)有助于细粒度的局部搜索。这种结构反映了在社交网络中观察到的“小世界”现象,其中任何两个节点之间存在短路径长度

github地址

--

From 日报小组 BobQ, FBI小白

社区学习交流平台订阅:

]]>
2024-03-18 10:19:07
美团转正实习招聘 base北京/上海/成都/深圳https://rustcc.cn/article?id=5c6619fe-acb5-4152-b55b-3374632fd707【招聘岗位】:软件开发工程师 【招聘对象】:2025届毕业生(毕业时间2024年11月-2025年10月的国内外院校应届毕业生。) 【内推码】:FlnJylT 查看岗位具体JD以及投递简历可点击结尾处链接进行,欢迎加入美团~

]]>
2024-03-18 09:32:01
Tran v.0.2.8 发布了!https://rustcc.cn/article?id=7efec29c-09de-4b0b-95a1-98403e029a97Tran

简洁, 快速, 划词翻译

tran

更新

相比 0.2.5 版本, 近期的优化的部分有:

[0.2.8]

做一个简洁的应用往往就会这样, 功能实现之后除了更新依赖就没有太多变化了, 后续如果没有其他变化将维持一月一次更新依赖发包的频率

Tran 的开发告一段落了 🎈.

  • 修改应用图标
  • 更新依赖升级

[0.2.7]

  • 调整托盘菜单,添加 TG 群组链接和版本号
  • 修复某些时候单词翻译无分类时出现的空白类型
  • 修复隐藏窗口后翻译内容未清空的问题

[0.2.6]

  • 支持固定时双击选词翻译
  • 托盘菜单模式选项显示当前模式

Tran

简洁, 快速, 划词翻译

EN_README Rust Tauri Windows MacOS Linux LICENSE Downloads Telegram Cloudflare

Keep it simple,stupid.

你好

  • 开箱即用
  • 永久免费使用
  • 谷歌翻译镜像
]]>
2024-03-18 09:02:29
关于专科生的第一门编程语言是否选择Rust?https://rustcc.cn/article?id=573e4973-ad6c-45c1-a258-49d5477fb7c8我即将升上专科学校并选择了计算机相关的专业,对编程处于一种还没接触过的状态。所以想请教一下各位前辈,Rust 适不适合作为第一门编程语言?

]]>
2024-03-18 08:37:41
Handler<_, _>` is not satisfied\nthe following other types implement trait `Handler<T, S>`https://rustcc.cn/article?id=3c7e8229-95ae-4ad8-8ab3-8a7027014efc报错信息如下:

[{
	"resource": "/e:/Backend/src/routes.rs",
	"owner": "rustc",
	"code": {
		"value": "Click for full compiler diagnostic",
		"target": {
			"$mid": 1,
			"path": "/diagnostic message [2]",
			"scheme": "rust-analyzer-diagnostics-view",
			"query": "2",
			"fragment": "file:///e%3A/Backend/src/routes.rs"
		}
	},
	"severity": 8,
	"message": "the trait bound `fn(axum::Json<AcceptCardPayload>, Extension<Pool<ConnectionManager<PgConnection>>>) -> impl Future<Output = Result<axum::Json<ApiResponse<()>>, axum::http::StatusCode>> {accept_card_handler}: Handler<_, _>` is not satisfied\nthe following other types implement trait `Handler<T, S>`:\n  <axum::handler::Layered<L, H, T, S> as Handler<T, S>>\n  <MethodRouter<S> as Handler<(), S>>",
	"source": "rustc",
	"startLineNumber": 103,
	"startColumn": 42,
	"endLineNumber": 103,
	"endColumn": 61,
	"relatedInformation": [
		{
			"startLineNumber": 103,
			"startColumn": 37,
			"endLineNumber": 103,
			"endColumn": 41,
			"message": "required by a bound introduced by this call",
			"resource": "/e:/Backend/src/routes.rs"
		},
		{
			"startLineNumber": 140,
			"startColumn": 16,
			"endLineNumber": 140,
			"endColumn": 29,
			"message": "required by a bound in `post`",
			"resource": "/c:/Users/Administrator/.cargo/registry/src/index.crates.io-6f17d22bba15001f/axum-0.7.4/src/routing/method_routing.rs"
		}
	]
}]

出现问题的代码login_handler,同类型的get函数都是正常的,一使用post就有问题

// 定义应用路由
pub fn app_router(pool: DbPool) -> Router {
    let cors = configure_cors();

    Router::new()
        .route("/api/login", post(login_handler))
        .route("/api/login", options(options_handler)) // 处理跨域资源共享(CORS)预检请求
        .layer(cors) // 添加跨域资源共享(CORS)中间件
        .layer(Extension(pool)) // 将数据库连接池作为共享状态

    // ... 其他路由 ...
}
// src/login.rs
use crate::auth::{generate_jwt, validate_user_credentials, validate_wechat_user, Claims};
use crate::db::DbPool;
use axum::{extract::Json, http::StatusCode, Extension};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
pub struct LoginRequest {
    username: Option<String>,
    password: Option<String>,
    wechat_code: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct LoginResponse {
    token: String,
}

pub async fn login_handler(
    Json(req): Json<LoginRequest>,
    Extension(pool): Extension<DbPool>,
) -> Result<Json<LoginResponse>, StatusCode> {
    // 判断是否有微信code
    if let Some(code) = req.wechat_code {
        // 验证微信用户
        match validate_wechat_user(&code, &pool).await {
            Ok(authenticated_user) => {
                // 生成token
                let claims = Claims::new(authenticated_user.id, 3600);
                let token = generate_jwt(&claims)?;
                Ok(Json(LoginResponse { token }))
            }
            Err(e) => Err(e),
        }
    }
    // 判断是否有用户名和密码
    else if let (Some(username), Some(password)) = (req.username, req.password) {
        // 验证用户名和密码
        match validate_user_credentials(username, password, pool).await {
            Ok(user_id) => {
                // 生成token
                let claims = Claims::new(user_id, 3600);
                let token = generate_jwt(&claims)?;
                Ok(Json(LoginResponse { token }))
            }
            Err(e) => Err(e),
        }
    }
    // 否则返回错误
    else {
        Err(StatusCode::BAD_REQUEST)
    }
}


尝试看了论坛里面另外一个同类报错,但是没找到解决思路

]]>
2024-03-18 08:28:51
serialize_with_bson bson DateTime 转json 帮助函数,转换指定时区和格式的时间格式https://rustcc.cn/article?id=ece40fd6-240f-4f2a-ad56-aa083b53d02bserialize_with_bson

bson DateTime 转json 帮助函数,转换指定时区和格式的时间格式

Example

use serialize_with_bson::{
    datetime_to_tz, datetime_to_tz_map, layout::DEFAULT, object_id_to_hex, time_zone_and_layout,
    TimeZoner,
};
use bson::{doc, oid::ObjectId, DateTime, Document};
use serde::{Deserialize, Serialize};
use serde_json::to_string;
use std::{collections::HashMap, str::FromStr};
 
    time_zone_and_layout!("Asia/Tokyo", DEFAULT,BsonDateTime); // 设定 DateTime的包装类型为BsonDateTime 设定时区和时间字符串输出格式

    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
    pub struct Bacterium {
        #[serde(rename = "_id", serialize_with = "object_id_to_hex")] // 启用自定义序列化函数 object_id_to_hex 把ObjectID转成hex字符串
        pub id: ObjectId,
        pub has_genome: bool,
        #[serde(serialize_with = "datetime_to_tz")] // 启用自定义序列化函数datetime_to_tz 把BsonDateTime转到指定时区时间
        pub creation_time: BsonDateTime,
        #[serde(serialize_with = "datetime_to_tz")]
        pub modified_time: BsonDateTime,
        pub short_id: String,
        #[serde(serialize_with = "datetime_to_tz_map")] // 启用自定义序列化函数datetime_to_tz_map 把Map中的BsonDateTime转到指定时区时间
        pub locations: HashMap<String, BsonDateTime>,
        pub taxonomy: Document,
        pub backtrace: Vec<String>,
    }

    
fn main() {
        let mut map: HashMap<String, BsonDateTime> = HashMap::new();
        map.insert(
            "R3R-A-9-2-L5".to_string(),
            DateTime::from_millis(1571985978429).into(),
        );
        map.insert(
            "R3T-A-5-3-K6".to_string(),
            BsonDateTime::from(DateTime::from_millis(1571984742668)),
        );
        map.insert(
            "R3R-A-10-3-L7".to_string(),
            BsonDateTime::from_millis(1571985444876),
        );

        let src = Bacterium {
            id: ObjectId::from_str("5db131829181e500010b93d6").unwrap(),
            has_genome: false,
            creation_time: DateTime::from_millis(1571893634109).into(),
            modified_time: DateTime::from_millis(1690439186944).into(),
            short_id: "B1DXX".to_owned(),
            locations: map,
            taxonomy: doc! {
              "class": "Bacilli",
              "phylum": "Bacillota",
              "kingdom": "Bacteria",
              "cnSpecies": "粪肠球菌",
              "species": "Enterococcus faecalis",
              "genus": "Enterococcus",
              "family": "Enterococcaceae",
              "order": "Lactobacillales"
            },
            backtrace: vec!["H2T73".to_owned(), "H2RNV".to_owned()],
        };
        if let Ok(result) = to_string(&src) {
            assert_eq!(true, true);
            println!("{:?}", src);
            println!("{}", result);
        }
    }

    output:
   {
	"_id": "5db131829181e500010b93d6",
	"has_genome": false,
	"creation_time": "2019-10-24 14:07:14.109 +0900 JST",
	"modified_time": "2023-07-27 15:26:26.944 +0900 JST",
	"short_id": "B1DXX",
	"locations": {
		"R3T-A-5-3-K6": "2019-10-25 15:25:42.668 +0900 JST",
		"R3R-A-9-2-L5": "2019-10-25 15:46:18.429 +0900 JST",
		"R3R-A-10-3-L7": "2019-10-25 15:37:24.876 +0900 JST"
	},
	"taxonomy": {
		"class": "Bacilli",
		"phylum": "Bacillota",
		"kingdom": "Bacteria",
		"cnSpecies": "粪肠球菌",
		"species": "Enterococcus faecalis",
		"genus": "Enterococcus",
		"family": "Enterococcaceae",
		"order": "Lactobacillales"
	},
	"backtrace": ["H2T73", "H2RNV"]
}

]]>
2024-03-18 04:06:26
兄弟们,postgresql一般选择哪种技术栈https://rustcc.cn/article?id=6757ad2c-add0-4f43-81d8-fda23c1223a6请教一下大家,axum框架下,postgresql一般选择哪种技术栈 比如: rust-postgres SQLx Diesel

]]>
2024-03-18 03:02:06
mongodb rust 驱动,ObjectId 和Datetime 字段转json字符串,必须自定义转换器实现。驱动默认真的没实现,也太麻烦了点?有更简单的方式吗?https://rustcc.cn/article?id=ca6abc00-4782-4847-99cf-b3a55e1a5506自定义序列化函数
use bson::{oid::ObjectId, DateTime};
use chrono::{FixedOffset, Utc};
use mongodb::{bson::doc, options, Client, Collection};
use serde::{Deserialize, Serialize, Serializer};
use serde_json::to_string;
use std::str::FromStr;

// 时间戳转UTC时间字符串
pub fn datetime_to_utc<S>(value: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let formatted = value.to_string();
    serializer.serialize_str(&formatted)
}

// 时间戳转北京时间字符串
pub fn datetime_to_cst<S>(value: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let d: chrono::DateTime<Utc> = value.to_system_time().into();
    let east_8 = FixedOffset::east_opt(8 * 3600).unwrap();
    let formatted = d.with_timezone(&east_8).to_string();
    serializer.serialize_str(&formatted)
}

// ObjectId 转 hex 字符串
pub fn object_to_hex<S>(value: &ObjectId, serializer: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    let formatted = value.to_hex();
    serializer.serialize_str(&formatted)
}

结构体定义

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Bacteria {
    #[serde(rename = "_id", serialize_with = "object_to_hex")]
    pub ID: ObjectId,
    pub hasGenome: bool,
    #[serde(serialize_with = "datetime_to_cst")]
    pub creationTime: DateTime,
}

解析打印的转json结果

{"_id":"5db131829181e500010b93d6",
"hasGenome":false,
"creationTime":"2019-10-24 13:07:14.109 +08:00"}

如果不启用自定义序列化器,结构体默认定义

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Bacteria {
    #[serde(rename = "_id")]
    pub ID: ObjectId,
    pub hasGenome: bool,
    pub creationTime: DateTime,
}

默认转换成json结果_id,creationTime的结果,跟MongoDB Compass 查询显示的不一致

{"_id":{"$oid":"5db131829181e500010b93d6"},
"hasGenome":false,
"creationTime":{"$date":{"$numberLong":"1571893634109"}}}
]]>
2024-03-12 08:21:02
感谢ChatGPT,救了我狗的命!https://rustcc.cn/article?id=4cb5180c-d393-417a-ad00-92b1baafbf17PS 前端后端测试,深圳武汉西安:jinshuju.net/f/o38ijj

前一段时间,国外一位小哥哥在推特上发布了一条消息,声称GPT-4拯救了自家狗狗的性命。 这是怎么一回事呢?

这个小哥哥养了一只两岁的边境牧羊犬,这只牧羊犬被诊断出患有蜱传疾病,这属于一种细菌性传染病。 虽然小哥哥一直带着狗狗积极治疗,但是狗狗的病情仍然在加重,出现了严重的贫血。对于狗狗病情,兽医一时也找不到真正的原因,尝试了许多治疗方法都没能得到缓解。 小哥哥眼看着可爱的狗狗一天一天衰弱下去,预计不久就要回归汪星了,感到很无力。 就在这时候,OpenAI的新版本大模型GPT-4刚刚发布,小哥哥抱着试一试的心态,把小狗的病情现状,以及各种详细的检查和诊断数据输入给ChatGPT,并且请AI给出诊断意见。

ChatGPT分析了这些数据,非常智能地罗列出了一些对病因的猜测。

在AI所列出的狗病原因列表中,GPT-4表示,免疫介导性溶血性贫血(IMHA)最符合小狗当前的症状。 这一结论点醒了男主,他火速联系上兽医,兽医也果断采取针对性的治疗。因为这一次的对症下药,狗狗的病情很快康复了。

在GPT-4给出正确分析,拯救了自己小狗之后,小哥哥感慨万千,于是有了前面推特上的那一番话。 看到这条新闻,小灰也感到很欣慰。有很多人总是视AI如洪水猛兽,觉得某一天AI强大起来,会取代我们的工作,甚至会颠覆人类社会。

其实,AI在更多的情况下是我们的好朋友,好帮手,而非我们的敌人。我相信,未来的AI不但可以拯救狗命,也会拯救更多人类的生命。我们的世界,会因为AI而变得更美好。​

]]>
2024-03-06 05:36:09
DELETEDhttps://rustcc.cn/article?id=90d42d78-6f5c-41dd-b3a8-b8bfb189e7712024-02-28 02:17:22Lightning Network Development Recruitmenthttps://rustcc.cn/article?id=fd3b7db4-8c35-43cc-89bc-de8347883c05responsibilities

As an Open Source RGB/Lightning Developer, you will be a part of the team that contributes to the development of the RGB protocol for assets on top of Bitcoin and the Lightning Network, aimed at building a new ecosystem of Bitcoin based decentralized financial applications. This includes:

  • Contributing to improve and evolve the code functional to the RGB protocol.
  • Development and maintenance of libraries to facilitate RGB integration for wallet developers.
  • Development and maintenance of an RGB compatible Bitcoin wallet.
  • Development and maintenance of Lightning Network and DEX compatible applications. requirements
  • Knowledgeable about Bitcoin, Lightning Network and related applications.
  • Knowledgeable about Rust, Docker and Linux Work experience: Rust > 2 years, able to provide personal github address.

Salary:4000-7000USD/month Telegram:@C89043

]]>
2024-02-20 02:50:13
Rust登陆【华为鸿蒙】操作系统之Native模块开发https://rustcc.cn/article?id=568d35d6-b782-49e9-b9b1-5d870d28f927Rust登陆【华为鸿蒙】操作系统之Native模块开发

名词解释

  • 【鸿蒙操作系统】的英文全名是Open Harmony Operation System。正文将以其首字母缩写词ohos引用该词条。
  • 【鸿蒙软件开发工具包】的英文全名是Open Harmony Software Development Kit。正文也将以它的首字母缩写词ohsdk引用该词条。
  • DevEco Studio IDE是【华为】为鸿蒙应用程序开发免费提供的集成开发环境。它的最新稳定版内置了ohsdk 3.1.0 (API v9)
  • Native模块】是指由遵循了ArkTs NAPI接口规范的C/Cpp/Rust程序经交叉编译输出的链接库.so文件。

前言

到写文章时止,虽然华为技术团队既未将rustup工具链无缝集成入DevEco Studio IDE也未提供ArkTs + Rust的“一站式”混合编程体验,但Rust登陆ohos依旧势不可挡,因为相较于Rust带来的生产效率收益(参照c / cpp),搭建交叉编译环境的人工成本真的微不足道。甚至,求助于【操作系统镜像】或Docker技术,@Rustacean 还能避免这类重复性劳动的再次发生。

为了填补DevEco Studio IDErustup工具链之间的“窄沟”,仅有两步操作需被执行:

  1. 搭建面向ohos的交叉编译环境。
    • 限于作者dev boxWindows 11,所以本篇文章仅分享从Windowsohos的交叉编译环境搭建心得。
  2. 将交叉编译输出的.so文件注入DevEco Studio工作流。

搭建Windowsohos交叉编译环境

鉴于华为硬件产品的三款主流CPU架构,@Rustacean 需同时准备三套交叉编译方案,分别是:

  • 面向64ARM CPUaarch64-unknown-linux-ohos方案。
  • 面向32ARM CPUarmv7-unknown-linux-ohos方案。
  • 面向64AMD / Intel CPUx86_64-unknown-linux-ohos方案。

前两套方案是为【真机】设备提供动态链接库/Native模块;而后一套方案则是服务于手机模拟器(虚拟机)的。

image

上表中Triple的信息描述格式统一是:

<CPU架构><CPU子架构>-<厂商>-<操作系统>-<应用程序二进制接口格式>

于是,armv7-unknown-linux-ohos应被读作

image

【厂商】栏的unkownMozilla公司的“锅”,而不是我定的。就我本意,这一栏馁馁的是汉语拼音HuaWei

下面上干货了...

第一步,给ohsdk补装native组件

DevEco Studio IDE的内置ohsdk位于%LocalAppData%\Huawei\Sdk\openharmony\<API 版本号>目录下,但其初始安装却缺失了native组件(— 可能是因为这个模块太大了,超过2GB)。所以,@Rustacean 需要

  1. 补装native组件
  2. 记住ohsdk对应的【API版本号】,因为后续配置得用。

具体步骤

  1. 打开DevEco Studio IDE

  2. 若出现的是【欢迎界面】,就从菜单ConfigureSettings,打开Settings对话框

  3. 若出现的是【工程界面】,就从菜单FileSettings,打开Settings对话框

  4. 从对话框左侧选择SDK;从右侧查看Platform选项卡下面的内容

  5. 寻找并记忆被勾选的【SDK版本号 (API版本号)】。比如,下图中的3.1.0 (API 9)

    image

  6. 勾选native复选框

  7. 点击OK按钮

  8. 等待native组件安装完成 — 耐心点儿,等待时间可不短

待上述操作都正常完成之后,便可见如下所示的新目录结构

image

第二步,重新编译Rust标准库

之所以把事情搞这么大是因为Mozilla厂方并没有为ohos提供预编译的【标准库】二进制文件。于是,尽管ohos已被纳入了rustc交叉编译支持清单(请见下图)

image

,但直接执行交叉编译指令

cargo build --release --target=aarch64-unknown-linux-ohos

还是会遭遇失败和看到E0463号错误

image

技术方案选型

编译【标准库】源码有两条技术路径

  1. 重新编译整条rustup工具链,捎带着也就编译出【标准库】了 — 难!我没搞定

  2. 将【标准库】作为普通依赖crateCargo (Lib) Package工程的业务代码一起编译(— 注:这个解释并不精确,因为细究起来crate依赖crates是搅和在一起的各自独立编译,而不是绝对意义上的“一锅烩”)。下图中被红框圈定的crates就都出自于【标准库】

    image

我选择了第二条技术路线。虽然后一条技术路线拖长了程序编译的总用时,但它仅会影响首次编译操作。从那以后,借助sccache编译缓存技术,由【标准库】引入的额外延时几乎可以忽略不计。更重要的是,该技术路线不会阻塞 @Rustacean 对rustup工具链的后续升级。咱们随时都可以rustup update

采用【方案二】的准备工作与先决条件

  1. rustup工具链,补装【标准库】源码(即,rust-src组件)。

    从命令行,立即执行且仅执行一次:

    rustup component add rust-src
    
  2. 启用nigtly工具链,因为工具链的stable版本还尚不支持“裹挟【标准库】共同编译”的新功能。

    从命令行,立即执行且仅执行一次:

    rustup default nightly
    
  3. 采用ohsdk内置的llvm - clang作为rustc链接器(下一节将详细介绍)

  4. 向交叉编译指令添加新命令行参数-Zbuild-std

    1. cargo会透传该参数给rustc并指示编译器不是寻找现成的【标准库】链接文件而是现场编译【标准库】源码。

    2. 编译指令也将变为

      cargo +nightly build -Zbuild-std --release --target=aarch64-unknown-linux-ohos
      

如何把ohsdk内置的llvm - clang作为rustc链接器

第一步,回忆之前记下的【鸿蒙API版本号】数字和新建环境变量OHOS_API_V。【推荐】从Cargo全局配置文件%UserProfile%\.cargo\config.toml新建OHOS_API_V环境变量,因为

  • 一方面,这可最小化对系统环境的“污染” — 该变量仅对Rust交叉编译有用,没有必要系统级全局可见。
  • 另一方面,它随时可被【会话级】同名环境变量短暂复写,方便以后临时变更做试验。

打开%UserProfile%\.cargo\config.toml配置文件和添加配置表

[env]
OHOS_API_V = "9"

【注意】伴随今后ohsdk自动升级,该环境变量的值须被同步地手动更新,以避免编译失败。

第二步,将ohsdk目录下的LLVM前端编译器llvm\bin\clang.exe包装为rustc的【鸿蒙链接器】。敲黑板,重点来了!@Rustacean 需分别构建三个链接器,以服务三套交叉编译方案,和向华为的三类硬件设备提供.so文件。于是,有

  • 【链接器1】面向64ARM CPU真机aarch64-unknown-linux-ohos交叉编译方案。在%UserProfile%目录下,新建cmd文件aarch64-unknown-linux-ohos-clang.cmd,并添加如下代码

    %LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\llvm\bin\clang.exe ^
    -target aarch64-linux-ohos ^
    --sysroot=%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\sysroot ^
    -D__MUSL__ %*
    
  • 【链接器2】面向32ARM CPU真机armv7-unknown-linux-ohos交叉编译方案。在%UserProfile%目录下,新建cmd文件armv7-unknown-linux-ohos-clang.cmd,并添加如下代码

    %LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\llvm\bin\clang.exe ^
    -target arm-linux-ohos ^
    --sysroot=%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\sysroot ^
    -D__MUSL__ ^
    -march=armv7-a ^
    -mfloat-abi=softfp ^
    -mtune=generic-armv7-a ^
    -mthumb %*
    
  • 【链接器3】面向64AMD / Intel CPU模拟器x86_64-unknown-linux-ohos交叉编译方案。在%UserProfile%目录下,新建cmd文件x86_64-unknown-linux-ohos-clang.cmd,并添加如下代码

    %LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\llvm\bin\clang.exe ^
    -target x86_64-linux-ohos ^
    --sysroot=%LocalAppData%\Huawei\Sdk\openharmony\%OHOS_API_V%\native\sysroot ^
    -D__MUSL__ %*
    

第三步,全局且有条件地向rustc装配【鸿蒙链接器】。其中,

  • 【全局】意味着修改Cargo全局配置文件%UserProfile%\.cargo\config.toml和作用于所有Cargo Package工程。
  • 【有条件】意味着采用条件编译语法target.<triple>.linker限定该【链接器】仅生效于面向ohos的交叉编译操作。

具体作法,打开%UserProfile%\.cargo\config.toml配置文件和添加配置表

[target.aarch64-unknown-linux-ohos]
linker = "./aarch64-unknown-linux-ohos-clang.cmd"
[target.armv7-unknown-linux-ohos]
linker = "./armv7-unknown-linux-ohos-clang.cmd"
[target.x86_64-unknown-linux-ohos]
linker = "./x86_64-unknown-linux-ohos-clang.cmd"
[profile.dev.package.compiler_builtins]
opt-level = 2

再对前面配置片段补充两点解释:

  1. 配置项linker相对路径引用链接器文件的背后逻辑是cargo总是以config.toml父文件夹(.cargo)所处目录为起点开始解析相对路径(,而不是以config.toml的同级目录为起点)。所以,本例中的./路径前缀对应的就是登录账号的根目录%UserProfile%
  2. 配置项opt-level,借助【Profile重写(i.e. Override)】配置表头[profile.dev.package.compiler_builtins],仅将【开发编译】模式下【标准库】内compiler_builtins crate的代码优化级别强制锚定于2。否则,cargo build -Zbuild-std --target=aarch64-unknown-linux-ohos指令(注意:没有--release参数)会概率性地失败于exit code: 0xc0000005, STATUS_ACCESS_VIOLATION错误。

第四步,给冗长的交叉编译指令约定(短)别名。

还是打开%UserProfile%\.cargo\config.toml配置文件和增补如下配置表

[alias]
ohos-build = ["build", "-Zbuild-std", "--target=aarch64-unknown-linux-ohos", "--target=armv7-unknown-linux-ohos", "--target=x86_64-unknown-linux-ohos"]

于是,只要执行一条cargo ohos-build指令就相当于连续执行下面三条编译指令:

  1. cargo build -Zbuild-std --target=aarch64-unknown-linux-ohos
  2. cargo build -Zbuild-std --target=armv7-unknown-linux-ohos
  3. cargo build -Zbuild-std --target=x86_64-unknown-linux-ohos

总结交叉编译环境的搭建成果

以后每次在Cargo (Lib) Package工程根目录下执行

cargo ohos-build --release

,编译器都会立即

  1. 唤起ohsdk内置的LLVM前端编译器llvm - clang作为rustc链接器
  2. 将【标准库】源码作为普通依赖cratecrate业务程序一起编译
  3. 并行启动三个JOB进程对同一套Rust源码同时执行三组交叉编译操作
  4. 交叉编译输出三个文件名相同ABI格式不同的动态链接库.so文件

新建Cargo (Library) Package工程,验证交叉编译环境

首先,克隆stuartZhang/socket2至本地,并将代码分支切至v0.4.x

git clone git@github.com:stuartZhang/socket2.git
cd socket2
git checkout -q v0.4.x

关于这一步操作的必要性,我已经详细地阐述于ohos-node-bindgen还不能被直接使用章节了。简单地讲,这是为了绕过socket2 crate对华为鸿蒙操作系统的不兼容缺陷。

然后,从命令行,新建Cargo (Library) Package工程

cd ..
cargo new --lib calculator
code calculator

其次,在VSCode内,打开Cargo.toml文件,和追加如下内容

[lib]
crate-type = ["cdylib"]

[dependencies]
ohos-node-bindgen = "6.0.3"
socket2 = "0.4.10"

[patch.crates-io]
socket2 = { path = "../socket2" }

前面配置片段内的【依赖图重写】配置表[patch.crates-io]指示Cargo包管理器使用本地的stuartZhang/socket2 crate山寨货替换crates.io上的正品,因为正品不兼容华为操作系统。

接着,从VSCode打开src/lib.rs文件,和增补如下Demo代码。这是一段简单的整数加运算程序。请把注意力聚焦在【派生宏】的使用上

use ::ohos_node_bindgen::derive::ohos_node_bindgen;
#[ohos_node_bindgen]
fn add(first: i32, second: i32) -> i32 {
    first + second
}

再次,执行交叉编译

cargo ohos-build --release

最后,从【资源管理器】查看编译输出结果

Cargo (Library) Package 工程根目录
├── Cargo.toml
├── src — Rust 源码目录
├── target
│  ├── aarch64-unknown-linux-ohos
│  │  └── release
│  │     └── libcalculator.so
│  ├── armv7-unknown-linux-ohos
│  │  └── release
│  │     └── libcalculator.so
│  ├── x86_64-unknown-linux-ohos
│  │  └── release
│  │     └── libcalculator.so

值得注意的是,编译输出的链接库文件名是lib前缀的。所以,Native模块的文件名是lib<包名>.so,而不是<包名>.so

Native模块注入普通的DevEco Studio工程

Native模块就是由前面交叉编译输出的ArkTs N-API链接库.so文件。

首先,从DevEco Studio IDE新建/打开普通Empty Ability工程。

然后,修改模块级build-profile.json5文件(比如,entry/build-profile.json5),和添加如下配置项至buildOption节点

"externalNativeOptions": {
  "abiFilters": [
    "arm64-v8a",
    "armeabi-v7a",
    "x86_64"
  ]
}

其次,在模块根目录下,创建下面三个子文件夹

  • libs/arm64-v8a
  • libs/armeabi-v7a
  • libs/x86_64

接着,依次向它们复制入编译好的链接库文件。例如,

image

最后,在ArkTs业务代码内(比如,entry/src/main/ets/pages/Index.ets),以ES Module语法,导入Native模块,和调用其成员方法

import calculator from 'libcalculator.so';
const result = calculator.add(2, 3);

总的来讲,调用端的ets代码就这么简单!但还是有三处优化可做以改善开发体验:

优化DevEco Studio工程目录结构

Cargo (Lib) PackageDevEco Studio Project合并为一个工程更有利于提高Rust + ArkTs的混合编程生产力。所以,如下DevEco Studio工程目录结构是被强力推荐的:

DevEco Studio 工程根目录
├── entry — 模块根目录
│   ├── libs — 交叉编译输出的 .so 文件都被复制到下面的子文件夹内
│   │   ├── arm64-v8a
│   │   ├── armeabi-v7a
│   │   └── x86_64
│   ├── src
│   │   ├── main
│   │   │  ├── resources
│   │   │  ├── cpp  — *旧有*的 Cpp(ArkTs N-API) 工程目录
│   │   │  ├── ets  — *旧有*的 ArkTs 源码目录
│   │   │  ├── rust — *新建*的 Rust(ArkTs N-API) 工程目录
│   │   │  │   ├── Cargo.toml
│   │   │  │   ├── src — Rust 源码目录
│   │   │  │   ├── target
│   │   │  │   │  ├── aarch64-unknown-linux-ohos
│   │   │  │   │  │  └── release
│   │   │  │   │  ├── armv7-unknown-linux-ohos
│   │   │  │   │  │  └── release
│   │   │  │   │  ├── x86_64-unknown-linux-ohos
│   │   │  │   │  │  └── release

Cargo (Lib) Package降级为DevEco Studio Project内某个特定模块下的子工程有两个好处:

  1. 同一个DevEco Studio工程内可同时包含多个Native子工程。
  2. 每个Native子工程既可独占一个模块以达成与主模块业务代码有限隔离的目的,也能与ets程序“混住”耦合于相同模块内。

友情提示

在移动Cargo (Lib) Package工程位置后,千万别忘了同步修改Cargo.toml配置文件中【依赖图重写】配置表[patch.crates-io]对本地stuartZhang/socket2 crate的引用路径。否则,会编译失败!

若假设stuartZhang/socket2 crateDevEco Studio Project处理于平级目录,那么【依赖图重写】配置表应该改为

[patch.crates-io]
socket2 = { path = "../../../../../socket2" }

自动化链接库.so文件的复制操作

在每次执行cargo ohos-build --release指令之后都徒手复制三个.so文件至不同的文件夹是非常低效的,所以 @Rustacean 有必要给Cargo编写build.rspost_build.rs构建程序,以扩展包管理器在编译前编译后的处理行为,并自动完成文件复制操作。其中,

  1. build.rs作为【前置处理】程序
    1. 从环境变量,收集.so文件的位置信息
    2. 生成[CMD] COPY /Y[Shell] cp -f文件复制指令
    3. 将【文件复制】指令尾追加至同一个.cmd / .sh脚本文件
  2. post_build.rs作为【后置处理】程序
    1. 执行被写入【文件复制】指令的程序文件,并
    2. 删除该程序文件

【打广告】build.rspost_build.rs皆未对上下文做任何的假设。所以,它们可被零成本地复用于其它同类工程中。

还是看图吧,一图抵千词

image

设计很完美但现实很骨感,因为Mozilla厂方的rustup工具链尚支持【后置处理】。所以,@Rustacean 需

  1. 额外安装功能增补包cargo-post

    cargo install cargo-post
    
  2. 修改Cargo全局配置文件%UserProfile%\.cargo\config.toml中的ohos-build别名设置,以使cargo-post生效

    [alias]
    ohos-build = ["post", "build", "-Zbuild-std", "--target=aarch64-unknown-linux-ohos", "--target=armv7-unknown-linux-ohos", "--target=x86_64-unknown-linux-ohos"]
    

    【注意】在"build"左侧添加了"post"数组项

Native模块导出接口,添加.d.ts类型提示

DevEco Studio IDE并没有集成类似于DLL Export Viewer的【动态链接库外部接口反射工具】。所以需要

  1. @Rustacean 在输出.so文件的同时也提供一份接口类型说明的.d.ts文件(— 其功能几乎等效于C头文件),并
  2. 将该类型说明文件注入DevEco Studio工作流

接下来,我沿着前面Rust + ArkTs混合编程的目录结构,描述操作步骤:

  1. 在模块entry的根目录下,创建src/main/rust/types/libcalculator子目录。注意:路径末端的文件夹名libcalculator是链接库文件的basename

  2. 在新建文件夹内,再新建文件index.d.ts和添入Native模块导出函数的函数签名

    export const add: (frist: number, second: number) => number;
    
  3. 接着新建文件oh-package.json5和添入Native模块的摘要信息。

    {
        "name": "libcalculator.so",
        "types": "./index.d.ts",
        "version": "0.1.0",
        "description": "ArkTs NAPI 原生模块示例"
    }
    

    其中,

    1. name字段就是链接库的文件名(含扩展名)。
    2. types字段是指向类型说明文件的相对路径。
    3. version字段是Native模块版本号。【推荐】该字段值与Cargo (Lib) Package子工程中Cargo.toml配置文件内[package]配置表下version配置项的值保持一致 — 这又是一处纯人工同步点。
    4. description字段是Native模块描述信息。
  4. 打开entry模块的oh-package.json5文件,并添加对Native模块的依赖项条目。

    "dependencies": {
        "libcalculator.so": "file:src/main/rust/types/libcalculator"
    }
    

    在依赖项条目中,左侧是链接库的文件名;而右侧是指向了类型说明文件所处文件夹的相对目录。

    image

  5. 最后,从DevEco Studio IDE依次点击菜单项BuildRebuild Project重新构建整个工程和使配置项修改生效。

于是,鸿蒙应用软件开发程序员就能在etsts代码编辑器内获得针对Native模块API的丰富类型提示了。

线上例程

我已将上述全部文字描述内容都例程化到github工程Arkts-NAPI-Rust-Demo内了。线下运行该工程可加强对文章繁杂内容的理解。

运行例程工程的环境要求

  1. rustc 1.75.0-nightly
  2. VSCode 1.86
  3. ohsdk 3.1.0(API v9)
  4. DevEco Studio 3.1.1 Release

运行例程工程的具体步骤

  1. 克隆git@github.com:stuartZhang/Arkts-NAPI-Rust-Demo.git

  2. VSCode内,

    1. 打开entry/src/main/rust目录
    2. 敲击Alt + T + R键。
    3. Command Palette下拉列表,依次点击buildohos-build--release
    4. 观察控制台输出日志,等待交叉编译结束。
  3. DevEco Studio IDE内,

    1. 打开工程根目录
    2. 启动手机模拟器
    3. 敲击Shift + F10键,运行移动端程序

    image

结束语与扩展阅读

搞定【交叉编译】难关仅只是鸿蒙Rust原生开发万里征程的第一步。加深对ArkTs - NAPI接口定义的理解才是【形成生产力】的核心任务。好消息是

  1. ArkTs - NAPInodejs N-API高度相似。至少截至目前,它们的相似度还>= 95%。所以,已熟悉nodejs原生模块编程的“老司机”们上手鸿蒙ArkTs - NAPI应该不难。

  2. 另外,我在春节假期期间贡献的ohos-node-bindgen crate更可大幅降低ArkTs - NAPI原生开发的复杂度。请对比下图左右侧的代码量

    image

    所以,ohos-node-bindgen crate值得大家点star呀!也请大家给Arkts-NAPI-Rust-Demostar

ohos-node-bindgen摘要

]]>
2024-02-16 09:28:23
Tran v.0.1.11 发布了!https://rustcc.cn/article?id=fa527cf0-9899-4ccb-89a6-779c26ed26d4Release v.0.1.11

已发布 v0.1.11 版本!相比一个月前的 v0.1.9 版本 平均反应速度提升至少 80ms 主要改进:

  • 支持多显示器
  • 去除 Tray 将 退出 功能合并到面板中
  • 设置界面, 按钮 hover 效果
  • 避免多次启动
  • 锁定快捷键, 去除无关的按键模拟, 提升快捷键响应速度
  • 快快快, 快就完了

tran-exit

Tran

简洁, 快速, 划词翻译

LICENSE Downloads Rust Tauri Windows MacOS Linux

Keep it simple,stupid.

功能

划词翻译 划过固定
translate drag
划过关闭 划过复制
close copy

快捷键: Alt + X

]]>
2024-01-19 02:31:27
smartscp: better scphttps://rustcc.cn/article?id=901dbd84-0891-4d9d-862c-ae0d19e1b1ef
  • 对scp的封装, 自动跳过被 gitignore 的文件
  • 原理 - 用 sshfs 把远程目录挂在本地的临时目录 - 用 xcp 复制文件
  • 之前我尝试过用 sftp 传输文件, 但是 sftp 很难控制远程 git 仓库 后来我发现 sshfs 可以操控远程 git 仓库像本地一样简单 因此切换到了 sshfs

    • 基本用法

      • 和 scp 一样, 不过传目录不需要 -r 参数
      • smartscp remote-host:remote_path local_path
        smartscp local_path remote-host
        smartscp local_path remote-host:remote_path
        
      • 如果目标目录是缺省的, smartscp 会自动根据原目录相对于 home 的偏移量, 计算目标目录
        • 比如source 是 ~/foo/bar, 那会自动传到远程的 ~/foo/bar
        • smartscp ~/foo/bar remote-host
          
    • 使用场景

      • 传 rust 项目的时候,避免传 target/
      • 传 nodejs 项目的时候, 避免传 node_modules/
      • 对于带宽 网速有限的网络环境, 特别有用
    • 尚不支持的功能

      • 包含 : 的文件名
    • 注意

      • 不兼容 scp 的参数, 比如不接受 -r 参数
      • 不要直接替代 scp
    • Q&A

      • 为什么不用 rsync --exclude=
        • 我并不觉得方便
        • 不够自动化
        • 要支持复杂的 gitignore
      • 为什么用rust写 - 为了在源码级依赖 xcp
      • 为什么不从头用rust写一遍 scp, 而不是调用 scp
        • 需要不少工作量
      • 为什么不用 c 直接改 scp
        • c 中不太方便导库
    ]]>
    2024-01-18 05:30:56
    更快的 tsv 解析https://rustcc.cn/article?id=054fabfd-ebd2-4973-898a-abff740e8ed9
  • 更快的 tsv 解析
  • 最近在B站冲浪时发现一个 Rust 和 Go 解析 tsv 文件的视频, 作者需要解析使用 get-NetTCPConnection | Format-Table -Property LocalAddress,LocalPort,RemoteAddress,RemotePort,State,OwningProcess 获取的本地所有 TCP 连接信息, 文件输出大致如下

    LocalAddress                          LocalPort RemoteAddress                     RemotePort       State OwningProcess
    ------------                          --------- -------------                     ----------       ----- -------------
    192.168.1.4                               54339 104.210.1.98                             443 Established          4504
    

    视频作者使用 regex 正则库处理输出, 发现比 Go 版本慢, 优化后虽然比 Go 快, 但并没有领先多少, 于是我自己尝试使用别的优化方法, 解析耗时能优化使用正则解析的 10% 左右. 下面来看看我的优化过程.

    项目搭建

    进行性能时建议使用 criterion, 它帮我们解决了性能的内存预加载, 操作耗时, 性能记录, 图表输出等功能.

    cargo new --lib tsv
    cd tsv
    cargo add criterion --dev -F html_reports
    cargo add regex
    

    然后在 Cargo.toml 里添加如 bench 文件

    [[bench]]
    name = "parse"
    harness = false
    
    // benches/parse.rs
    #![allow(dead_code)]
    use criterion::{black_box, criterion_group, criterion_main, Criterion};
    
    const OUTPUT: &str = include_str!("net.tsv");
    
    fn criterion_benchmark(c: &mut Criterion) {
        todo!()
    }
    
    criterion_group!(benches, criterion_benchmark);
    criterion_main!(benches);
    

    测试使用的 tsv 一共 380 行.

    regex 解析

    使用正则解析的正则表达式很简单, 这里直接给代码, 为了避免重复编译正则表达式和重新分配内存报错结果列表, 这里将她们作为参数传给解析函数.

    struct OwnedRecord {
        local_addr: String,
        local_port: u16,
        remote_addr: String,
        remote_port: u16,
        state: String,
        pid: u64,
    }
    fn regex_owned(input: &str, re: &regex::Regex, result: &mut Vec<OwnedRecord>) {
        input.lines().for_each(|line| {
            if let Some(item) = re.captures(line).and_then(|captures| {
                let (_, [local_addr, local_port, remote_addr, remote_port, state, pid]) =
                    captures.extract();
                let ret = OwnedRecord {
                    local_addr: local_addr.to_string(),
                    local_port: local_port.parse().ok()?,
                    remote_addr: remote_addr.to_string(),
                    remote_port: remote_port.parse().ok()?,
                    state: state.to_string(),
                    pid: pid.parse().ok()?,
                };
                Some(ret)
            }) {
                result.push(item);
            }
        });
        assert_eq!(result.len(), 377);
    }
    

    parse.rs 文件里要加上使用的正则和提前创建好列表, 并且将函数添加的 bench 目标里

    fn criterion_benchmark(c: &mut Criterion) {
        let re = regex::Regex::new(r"(\S+)\s+(\d+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\d+)").unwrap();
        let mut r1 = Vec::with_capacity(400);
        c.bench_function("regex_owned", |b| {
            b.iter(|| {
                // 重置输出 vector
                r1.clear();
                regex_owned(black_box(OUTPUT), &re, &mut r1);
            })
        });
    }
    

    接着跑 cargo bench --bench parse 进行测试, 在我的电脑上测得每次运行耗时 450 µs 左右.

    减少内存分配

    一个最简单的优化是使用 &str 以减少每次创建 String 带来的内存分配和数据复制.

    struct Record<'a> {
        local_addr: &'a str,
        local_port: u16,
        remote_addr: &'a str,
        remote_port: u16,
        state: &'a str,
        pid: u64,
    }
    

    两个函数代码差不多, 所以这里不再列出来, 可以通过 gits: tsv 解析 获取完整代码.

    可惜这次改动带来的优化非常小, 在我的电脑上反复测量, 这个版本耗时在 440 µs 左右.

    使用 ascii 正则

    rust 的 regex 正则默认使用 unicode, 相比于 ascii 编码, unicode 更复杂, 因此性能也相对较低, 刚好要解析的内容都是ascii字符, 使用 ascii 正则是否能提升解析速度呢? regex 有 regex::bytes 模块用于 ascii 解析, 但为了适配字段, 这里不得不使用 transmute&[u8] 强制转换成 &str

    fn cast(data: &[u8]) -> &str {
        unsafe { std::mem::transmute(data) }
    }
    fn regex_ascii<'a>(input: &'a str, re: &regex::bytes::Regex, result: &mut Vec<Record<'a>>) {
        input.lines().for_each(|line| {
            if let Some(item) = re.captures(line.as_bytes()).and_then(|captures| {
                let (_, [local_addr, local_port, remote_addr, remote_port, state, pid]) =
                    captures.extract();
                let ret = Record {
                    local_addr: cast(local_addr),
                    local_port: cast(local_port).parse().ok()?,
                    remote_addr: cast(remote_addr),
                    remote_port: cast(remote_port).parse().ok()?,
                    state: cast(state),
                    pid: cast(pid).parse().ok()?,
                };
                Some(ret)
            }) {
                result.push(item);
            }
        });
        assert_eq!(result.len(), 377);
    }
    

    添加到 bench 后性能大概多少呢?, 很遗憾, 性能与 regex_borrow 差不多, 在 430 µs 左右.

    抛弃 regex

    鉴于内容格式比较简单, 如果只使用 rust 内置的 split 等方法解析性能会不会更好呢? 解析思路很简单, 使用 lines 得到一个逐行迭代器, 然后对每行使用 split 切分空格再逐个解析即可

    fn split<'a>(input: &'a str, result: &mut Vec<Record<'a>>) {
        input
            .lines()
            .filter_map(|line| {
                let mut iter = line.split([' ', '\t', '\r']).filter(|c| !c.is_empty());
                let local_addr = iter.next()?;
                let local_port: u16 = iter.next()?.parse().ok()?;
                let remote_addr = iter.next()?;
                let remote_port: u16 = iter.next()?.parse().ok()?;
                let state = iter.next()?;
                let pid: u64 = iter.next()?.parse().ok()?;
                Some(Record {
                    local_addr,
                    local_port,
                    remote_addr,
                    remote_port,
                    state,
                    pid,
                })
            })
            .for_each(|item| result.push(item));
        assert_eq!(result.len(), 377);
    }
    

    注意 line.split 只后还需要过滤不是空白的字符串, 这是因为字符串 "a b" split 之后得到 ["a", "", "b"].

    经测试, 这个版本测试耗时大概为 53 µs, 这真是一个巨大提升, rust 的 regex 性能确实有些问题.

    每次 split 之后还需要 filter 感觉有些拖沓, 刚好有个split_whitespace, 换用这个方法, 将新的解析方法命名为split_whitespace后再测试下性能

    let mut iter = line.split_whitespace();
    

    令人意想不到的是性能居然倒退了, 这次耗时大概 60 µs, 仔细研究下来还是 unicode 的问题, 改用 ascii 版本的 split_ascii_whitespace 之后性能提升到 45 µs.

    手写解析状态机

    除了上述的方法, 我还尝试将 Record 的 local_addr 和 remote_addr 改成 std::net::IpAddr, 消除 next()?.parse().ok()? 等其他方法, 但收益几乎没有, 唯一有作用的办法是手写解析状态机.

    大致思路是, 对于输出来说, 我们只关系它是以下三种情况

    1. 换行符 NL
    2. 除了换行符的空白符 WS
    3. 非空白字符 CH

    只解析 LocalAddr 和 LocalPort 解析状态机如下, 如果要解析更多字段, 按顺序添加即可.

    stateDiagram-v2
        [*] --> LineStart
        LineStart --> LineStart:WS
        LineStart --> ConsumeLocalAddr:Ch
        ConsumeLocalAddr --> LineStart:NL
        ConsumeLocalAddr --> LocalAddr:WS
        ConsumeLocalAddr --> ConsumeLocalAddr:CH
        LocalAddr --> LineStart:NL
        LocalAddr --> LocalAddr:WS
        LocalAddr --> ConsumeLocalPort:CH
        ConsumeLocalPort --> ParsePort:NL
        ConsumeLocalPort --> ParsePort:WS
        ConsumeLocalPort --> ConsumeLocalPort:CH
        ParsePort --> LineDone:parse ok
        ParsePort --> Abort:parse error
        LineDone --> LineStart:NL
        Abort --> LineStart:NL
        LineDone --> [*]:EOF
        Abort --> [*]:EOF
    

    因为代码有些复杂, 所以这里不再贴出来, 完整代码在 gits 上. 手写状态机的版本耗时大概在 32 µs 左右. 这版本主要性能提升来自手写状态机减少了循环内的分支判断.

    SIMD 加速?

    在上面手写解析的例子里, 处理过程类似与将输出作为一个 vec, 状态机作为另一个 vec, 将两个 vec 进行某种运算后输出结果, 应该能使用 simd 进行加速, 但我还没想出高效实现. 所以这里只给出可能的参考资料

    1. zsv 使用 simd 加速的 csv 解析库
    2. simd base64 一篇介绍使用 simd 加速 base64 解析的博客, 非常推荐

    总结

    rust regex 在某时候确实存在性能问题, 有时候使用简单的 split 的方法手动解析反而更简单性能也更高, 如果情况允许, 使用 ascii 版本能进一步提升性能, 如果你追求更好的性能, 手写一个状态不失为一种选择, 当然我不建议在生产上这么做. 同时我也期待有 simd 加速的例子.

    公众号原文

    ]]>
    2023-12-26 10:34:55
    Rust与面向对象(四)https://rustcc.cn/article?id=d9f464f8-b7c8-4843-975d-1619fae771e7原型法

    此原型法非原型模式,而是类似JavaScript中的原型扩展,在JS中,能够很轻松地为String类型“原地”扩展方法,如:

    String.prototype.isDigit = function() {
      return this.length && !(/\D/.test(this));
    };
    

    这个能力其实很好用,但是C++无法这样,一直觉得std::string的功能不足,想为其添加更丰富的如trim/split之类的语义,只能采用继承或者组合代理方式:

    • 继承:用一个新类继承std::string,并为新类实现trim/split
    • 组合代理:用一个新类组合std::string,并为新类代理所有std::string的方法,包括各类构造方法和析构方法,再为新类实现trim/split

    然后,使用std::string的地方替换成新类。这时候那种都比较复杂,组合的方式更复杂一些,所以也别无脑相信面向对象里“组合一定优于继承”。幸运的是,Rust能轻易完成原型法,比如有个bytes库提供了可廉价共享的内存缓冲区,避免不必要的内存搬运拷贝,bytes::BytesMut实现了可变缓冲区bytes::BufMut,有一系列为其写入u8、i8、u16、i16、slice等基础类型的接口,对于基础的通用的在bytes库中已经足够了,现在有个网络模块,想往bytes::BytesMut中写入std::net::SocketAddr结构,Rust可轻易为BytesMut扩展实现put_socket_addr

    pub trait WriteSocketAddr {
        fn put_socket_addr(&mut self, sock_addr: &std::net::SocketAddr);
    }
    
    impl WriteSocketAddr for bytes::BytesMut {
        fn put_socket_addr(&mut self, sock_addr: &std::net::SocketAddr) {
            match sock_addr {
                SocketAddr::V4(v4) => {
                    self.put_u8(4);        // 代表v4地址族
                    self.put_slice(v4.ip().octets().as_ref());
                    self.put_u16(v4.port()); 
                }
                SocketAddr::V6(v6) => {
                    self.put_u8(6);        // 代表v6地址族
                    self.put_slice(v6.ip().octets().as_ref());
                    self.put_u16(v6.port()); 
                }
            }
        }
    }
    

    然后就可以使用BytesMut::put_socket_addr了,只需use WriteSocketAddr引入这个trait就可以,是不是很轻松!为何会这么容易?先看JS的原型法,其背后是原型链在支撑,调用String的方法,不仅在String对象里面查找,还会层层向String的父级、祖父级prototype查找,一旦找到就可以调用,而每个prototype本质上都是个Object,可以获取并编辑它们,ES6的继承本质上也是原型链。所以可以拿到String类的prototype,在它上面为其增加isDigit,就能让所有的String对象都能享受isDigit函数的便利,可谓十分方便。但是C++就不行了,也想拿到std::string的函数表,然后一通编辑为其添加trim/split行为,奈何C++不允许这危险的操作啊,只能派生子类,即便子类仅仅只包含一个std::string。那Rust为何可以,关键就是trait函数表与传统面向对象的虚函数表解藕了,后果就是,类型没有绑死函数表,可以为类型增加新trait函数表,然后就有了上面的Rusty原型法。类似的还可以为Rust的String扩展is_digit/is_email/is_mobile,一样地简单。一般有ext模块,就很可能发现原型法的身影,比如tokio::io::AsyncReadExt

    原型法是最能体现trait函数表与传统面向对象虚函数表分离优势的设计模式!注意,Rust的原型法并没有产生任何新类型,只是增加了一个新的trait函数表,所以一开始称之为“原地”扩展,是比JS更干净的原型法,个人非常喜欢用这个模式,能用就用!更进阶的,Rust还能为所有实现了bytes::BufMut的类型扩展实现WriteSocketAddr特型,而不仅仅只为bytes::BytesMut实现:

    /// 可以这样读:为所有实现了ButMut特型的类型实现WriteSocketAddr
    /// bytes::BytesMut也不过是T的一种,代码复用性更佳
    impl<T: bytes::ButMut> WriteSocketAddr for T {
        fn put_socket_addr(&mut self, sock_addr: &std::net::SocketAddr) {
            // 同样的代码
        }
    }
    

    原型法跟模板方法还有些联系,也算模板方法衍生出来的设计模式,因为子类如果不依赖父类,并且子类还不需要有任何字段,不需要有自己独特的结构就能实现算法策略时,那子类也不用依赖注入到父类了,直接在父类的基础上“原地“扩展,更加轻量。总结一下模板方法的衍生变化:

    模板方法:

    • 子类拥有自己的结构,并依赖父类的结构和行为才能完成,是模板方法
    • 子类拥有自己的结构,但不依赖父类结构和行为也能完成,可不用继承转而采用组合依赖注入,最好多达2个以上组合,达成策略组合模式
    • 子类不需有自己的结构(或者一个空结构),依赖父类的结构和行为就能完成,只是算法在父类模块中不通用而没实现,可不用继承也不用组合,“原地”扩展,原型法即可
    • 子类不需有自己的结构,也不依赖父类,那这么独立也跟父类没任何关系了,理应属于其它模块

    回到面向对象,凡是Rust能轻松做到的,面向对象却无法轻松做到的,就是面向对象该被批评的点。。面向对象说我服,早知道也不把虚函数表与对象内存结构绑死了。所谓长江后浪推前浪,新语言把老语言拍死在沙滩上,即便C++20如此强大,不改变虚函数表的基础设计,在原型法上也永远追赶不上Rust语言的简洁。

    装饰器模式

    上节说到,策略模式,要是为复合类型也实现trait,就类似装饰器模式,因为装饰器无论是内部委托成员,还是外部装饰器自己,都得实现同一个名为Decorate的trait,就是为了让它们可以相互嵌套组合:

    trait Decorate {
        fn decorate(&mut self, params...);
    }
    
    /// 一个静多态的装饰器
    struct SomeDecorator<D: Decorate> {
        delegate: D,    // 必要的委托
        ...
    }
    
    /// 还得为Decorator自己实现Decorate特型
    impl<D: Decorate> Decorate for SomeDecorator<D> {
        fn decorate(&mut self, params...) {
            // 1. SomeDecorator itself do sth about params 
            self.do_sth_about_params(params...); // 这是真正要装饰的实现
            // 2. then turn self.delegate
            self.delegate.decorate(params...);    // 这一句都相同,1、2步的顺序可互换
        }
    }
    
    /// 另一个装饰器
    struct AnotherDecorator<T: Decorate> {
        delegate: T,
        ...
    }
    
    impl<D: Decorate> Decorate for AnotherDecorator<D> {
        fn decorate(&mut self, params...) {
            // 1. AnotherDecorator itself do sth about params 
            self.do_sth_about_params(params...);
            // 2. then turn self.delegate
            self.delegate.decorate(params...);    // 这一句都相同
        }
    }
    
    /// 必要的终结型空装饰器
    struct NullDecorator;
    
    impl Decorator for NullDecorator { /*do nothing*/ }
    
    /// 使用上
    let d = SomeDecorator::new(AnotherDecorator::new(NullDecorator));
    d.decorate();
    

    SomeDecorator/AnoterDecorator是真正的装饰器,会有很多个,功能各异,每个Decorator所包含的相应的结构可能也不同。装饰器在使用上,就像链表一样,一个处理完之后,紧接着下一个节点再处理,它把链表结构包含进了装饰器的结构里面,并用接口/trait来统一类型。上述实现有重复代码,就是调用委托的装饰方法,还能继续改进:

    /// 装饰的其实是一个处理过程
    trait Handle {
        fn handle(&mut self, params...);
    }
    
    trait Decorate {
        fn decorate(&mut self, params...);
    }
    
    /// 装饰器的终结
    struct NullDecorator;
    
    impl Decorate for NullDecorator {
        fn decorate(&mut self, params...) {
            // do nothing
        }
    }
    
    /// 通用型装饰器,像是链表节点串联前后2个处理器节点
    struct Decorator<D: Decorate, H: Handler> {
        delegate: D,
        handler: H,   // 这又是个干净的模板方法,将变化交给子类
    }
    
    /// 通用装饰器本身也得实现Decorate特质,可以作为另一个装饰器的D
    impl<D: Decorate, H: Handler> Decorate for Decorator<D, H> {
        fn decorate(&mut self, params...) {
            // 这两步可互换
            self.handler.handle(params);
            self.delegate.decorate(params);
        }
    }
    
    /// 下面的处理器只关注处理器自己的实现就好了
    struct SomeHandler { ... };
    
    impl Handler for SomeHandler { ... }
    
    struct AnotherHandler { ... };
    
    impl Handler for AnotherHandler { ... }
    
    /// 使用上
    let d = Decorator {
        delegate: Decorator {
            delegate: NullDecorator,
            handler: AnotherHandler,
        },
        handler: SomeHandler,
    };
    d.decorate(params...);
    

    可以看出,装饰器很像链表,emm...大家都知道链表在Rust中较复杂,那链表有多复杂,装饰器就有多复杂。上面的静多态实现也是不行的,不同的装饰器组合,就会产生不同的类型,类型可能随着Handler类型数目增加呈其全排列阶乘级类型爆炸,忍不了,必须得换用指针。装饰器模式,Rust实现起来不如传统面向对象,面向对象天然动多态,且Decorator继承可以让D、H两部分合为一体,让H也成装饰类的一个虚函数,都在this指针访问范围内,简单一些。而Rust将装饰器拆解成了链表型,将装饰器的底层结构还原了出来,确实装饰器可以用链表串联起各个处理器一个接一个地调用,效果一样的。只是面向对象技巧隐藏了链表的细节。

    不过Rust有个很牛逼的装饰器,就是迭代器的map、step_by、zip、take、skip这些函子,它们可以随意串联组合调用,本质就是装饰器,只不过仅限于用在迭代器场景。如果装饰器能这样实现,能惰性求值,也能够编译器內联优化,就太强了。不过,各个装饰器功能不同,恐怕不能像迭代器函子那样都有清晰的语义,因此没有统一的装饰器库。不过装饰器实现时,肯定可以借鉴迭代器的函子思路。这样一来的话,Rust的装饰器又丝毫不弱于传统面向对象的了。而且,高,实在是高,妙,实在是妙!

    /// 以下仅作摘选,让大家一窥迭代器函子的装饰器怎么玩的
    pub trait Iterator {
        type Item;
    
        // Required method
        fn next(&mut self) -> Option<Self::Item>;
    
        // Provided methods
        // 像下面这样的函数还有76个,每个函数都映射到一个具体的装饰器,它们都返回一个装饰函子impl Iterator<Item = Self::Item>
        // 装饰器函数基本都定义完了,未来无法扩展?还记得原型法吗,为所有实现了Iterator的类型实现IteratorExt
        // 仅挑选一个step_by作为案例
        #[inline]
        #[stable(feature = "iterator_step_by", since = "1.28.0")]
        #[rustc_do_not_const_check]
        fn step_by(self, step: usize) -> StepBy<Self>
        where
            Self: Sized,
        {
            StepBy::new(self, step)
        }
    }
    
    /// StepBy装饰器,如第一种实现那样的写法
    pub struct StepBy<I> {
        iter: I,    // 装饰器的delegate
        step: usize,
        first_take: bool,
    }
    
    /// 再为StepBy<I>实现Iterator
    impl<I> Iterator for StepBy<I>
    where
        I: Iterator,
    {
        type Item = I::Item;
    
        #[inline]
        fn next(&mut self) -> Option<Self::Item> {
            self.spec_next()
        }
    }
    
    // 使用上,有别于传统装饰器模式从构建上去串联,这是利用返回值链式串联,顿时清晰不少
    vec![1, 2, 3].iter().skip(1).map(|v| v * 2);
    

    小结

    至此,模板方法的变化告一断落。之前,有人说Rust不支持面向对象,导致Rust不好推广,实际上并不是,哪个OO设计模式Rust实现不了,还更胜一筹。因此,并非Rust不支持面向对象!有些设计模式,Rust天生也有,如:

    • 单例模式:其实单例模式如果不是为了懒加载,跟使用全局变量没啥差别;如果为了懒加载,那lazy_static或者once_cell就够用。(补充:标准库已经标准化成OnceLock了)
    • 代理模式:NewType模式作代理挺好;或者原型法“原地”扩展代理行为
    • 迭代器模式:Rust的迭代器是我见过最NB的迭代器实现了
    • 状态机模式:Rust语言官方文档中的NewType+enum状态机模式,这种静多态的状态机非常严格,使用上都不会出错,所有状态组合还可以用enum统一起来,比面向对象的状态机模式要好

    还有一些设计模式,跟其它模式很像,稍加变化:

    • 适配器模式:同代理模式差别不大,很可能得有自己的扩展结构,然后得有额外“兼容处理”逻辑来体现“适配”
    • 桥接模式:就是在应用策略模式
    • 过滤器模式:就是在应用装饰器模式

    还有一些设计模式,读者可自行用Rust轻松实现,如观察者模式之流。后续不会为这些设计模式单独成文了,除非它有点意思,访问者模式就还可以,只不过实际应用不咋多。有想用Rust实现哪个设计模式有疑问的,可留言交流。

    罗列所有设计模式没啥意思,我也无力吐槽这么多设计模式,至今很多人仍区分不清某些设计模式的区别,因为设计模式在描述它们的时候,云里雾里的需求描述,关注点、应用场景不一样云云,什么模式都得来一句让“抽象部分”与“实现部分”分离,跟都整过容一样相似的描述,让人傻傻分不清。至今我再看各种设计模式,想去了解其间区别,都觉得无聊了,浪费时间!被大众广泛记住的设计模式就那么几个,因为基础的设计就那么几个,当你在使用接口、指针/引用、组合的时候,其实就在不知不觉中使用设计模式了。

    上段是在批评设计模式没错,并不是说设计模式一无是处,能总结出模式作为编程界通用设计语言意义非凡。懂它肯定比不懂的强,要是都能区分清各类设计模式了,肯定是高手中的高手了,看懂这一系列文章不难。设计模式的套用,归根结底是为了代码复用,良好的可读性。大家看到相似模式的代码,一提那种设计模式就能明白。遗憾的是,即便是同一个设计模式,因为乱七八糟的类型、胡乱命名、粗糙的掺杂不少杂质的实现,为不停变化的需求弄的面目全非者,让人读起来,实在很难对的上有某种设计,这并非设计模式的锅,而是编程素质不专业、太自由发挥、总见多识少地自创概念/二流招式的毛病招致的。

    在这方面,Rust的解决方案 极具 吸引力。后续对比着面向对象,讲讲Rusty那味,味道不错但更难掌握,属于基础易懂,逻辑一多就复杂(废话)!

    ]]>
    2023-12-24 11:43:40