- 主题:51不出门瞎逛,在家看了一遍rust,感觉不可能替代C吧
我51第三天装了台3d打印机...
然后发现丫自带的上位机貌似挂了,
然后我本来就对他的上位机不太满意,准备直接上红米手机刷机klipper的方案。
然后发现得把手机的电池改成供电板直接供电。
然后发现成品的直供板做的都是些啥臭鱼烂虾,干脆自己画个。
然后因为我集成度太高板子空荡荡的,于是想着干脆塞个CAN吧。
然后想想CAN都塞了,干脆下位机也塞进去吧,
然后就到了现在了...
回到你这个问题,你对rust的理解还是太表面了。当然严格地说你说的本质上也没错,rust确实就是做的比c多一点,可以理解为c+。
这里的过程嘛,得稍稍说的久一点。C诞生于田园时代,那个时候大家是相信性本善的。其实那个年代诞生的几乎所有系统都这样,都是不怎么设防的。所以你会觉得C简单又强大,因为大家不需要付出安全这个overhead。
但现在参与IT的阿猫阿狗实在是太多了,现在必须假设大家是性本恶的。(包括主观上的恶,以及能力不足导致客观上的恶)。所以2000年之后几乎所有的系统都在增加安全性,编程语言当然也一样。然而始终没有诞生一个比较好的,不需要付出性能代价同时又很安全的语言。最接近的是c++。但c++的历史包袱太重,同时也塞入了太多很复杂的东西,什么都想要,导致c++变成了一坨shit。所以直到现在,在需要性能的领域,事实上大家还是只能用c,或者通过规定人为禁用c++一部分feature。这两个方法都不太好。
rust的诞生,本质上就是一次吸收了2000之后众多语言特性后,做出了很好取舍的工程实践。所以在研究语言的学院派眼里rust是很土鳖的东西。但在工程师手里它就是刚刚好的工具,它切实的找到了c和c++的平衡点,所以我觉得它可以称之为c+。
回到rust具体的问题上来说。rust主要的贡献,是引入了生命周期管理这个模型,从而让系统代替人去管理,它并不直接等于内存管理,而且事实上rust也无法绝对的杜绝内存泄漏。只是用rust写出内存泄露的程序很困难而已(没有足够的理解,一般人写不出来)。其他都是次要的改进,把它改造成一个更先进的c。比如模式匹配就是一种非常便利的写法,毕竟2023年了,还是if-then-else,一大堆switch-case的不仅仅是土鳖,更重要的是容易犯错误。rust是安全优先的理念,哪怕删掉生命周期管理,rust在整个语言设计上也是设计的更强调安全的语言。
最后你的问题我不是很懂。rust的函数当然可以返回值,而且比起c可以返回复杂的多的多的东西。
至于指针么,rust里面其实也有非常接近指针的东西,叫box。类型转换什么的当然也都是可以的。
【 在 dismoon 的大作中提到: 】
: B站现在里面的视频up主水货越来越多了,最近几天windows11内核用了rust刷屏,然后去官网看了一下帮助文件
: 目前来说,rust也就主打一个安全,指针不能瞎JB指了。
: 但是把函数返回值搞掉是做什么?
: ...................
--
FROM 180.111.26.*
不高端。我本来想收台工业级的3d打印机回来改造的。好不容易有台看中的聊了半天人家又不卖了。
一怒之下找个比较好的开源方案先搭起来后面再改造。
你这个学了一天也没法多要求你什么...rust确实学习曲线还是比较陡的...
但你想下,这么典型的需求,怎么可能支持不了...
我先贴段代码:
#[derive(Debug)]
enum SafeIntOrFloat {
Float(f32, f32),
Integer(i32, i32),
}
union IntOrFloat {
i: u32,
f: f32,
}
fn main() {
let mut a: SafeIntOrFloat = SafeIntOrFloat::Float(0.1, 0.2);
println!("enum: {:?}", a);
a = SafeIntOrFloat::Integer(2, 3);
println!("enum: {:?}", a);
let mut b: IntOrFloat = IntOrFloat { i: 1 };
b.i = 2;
let i = unsafe { b.i };
let f = unsafe { b.f };
println!("union: {} {}", i, f);
}
这段代码要讲2个事情,首先rust可以随时用unsafe退回到c状态,比如c版本的union在rust里面也叫union,只不过是unsafe的,需要封装一下把unsafe包起来,如这段代码所演示的。你要是嫌封装unsafe是脱裤子放屁,那可以直接把所有代码都用unsafe来写,只不过这样用rust是为了图啥呢...
其次rust里更好的做法是不要用union这么原始的(直接操作内存的)东西,而是应该用enum。enum的作用和union本质上是一样的,也是同一块内存,记录不同类型的数据。但它已经封装好了不需要你去考虑具体的内存问题。当你需要把enum转换成数据流发出去的时候,你给他整一个serializer/deserializer。这才是更为通行的做法。
当然这个做法性能是有损失的,serdes一般来说会有协议结构,会有内存复制。而c下面的做法其实本质上就是个dummy serdes,也即直接把内存指针指向的内容丢出去。但这个方法本身就是诸多问题的根源,诸如alignment,big/little endian等等。rust里面理论上也可以做个类似的dummy serdes,但enum是有额外的内存占用来记录更多的meta信息,我相当怀疑这么搞的通用性。
总的来说,田园时代就是眉毛胡子一把抓,而现在系统复杂了之后是很讲抽象和设计的。好的设计可以把问题消灭于无形。比如你用c处理网络数据的时候要手动做big/little endian转换,这个本质上就是人肉实现的serdes,这种东西本身就不应该让人去操心。
【 在 dismoon 的大作中提到: 】
: 什么高端3D打印机还有上位机?
: 现在的3D打印机不都是直接弄个TF卡上面拷了STL或者STEP就能打印的么
: 说到rust的指针,我就举个例子
: ...................
--
修改:lvsoft FROM 180.111.26.*
FROM 180.111.26.*
其实归根结底就是看自己的需求。
嵌入式场景有几个特点,一个是规模很小,二是死扣成本,三是一次部署一万年不管。
所以对编程语言不是强需求。毕竟良好的设计,那是需要在不断迭代的复杂项目中才能体现出价值的。
嵌入式场景一般是技术跟着成本走。让你用4bit mcu写汇编你也得上...
但我是觉得随着硬件的提升,很多场景其实可以不用这么抠了,损失些性能,去交换更好的安全性,以及对接生态更庞大的平台,其实也不是啥坏事。
比如spacex能在龙飞船里面用javascript做UI...这个虽然有点步子迈太大有扯到蛋的风险,但我还是认同至少应该是这个大方向。我现在大部分项目已经用嵌入一个超轻量化的浏览器,同时这个浏览器又足够强可以跑vue这样的前端的方式了。现在我就舒服的很,可以跟GUI开发的一摊子烂事彻底说拜拜了。
【 在 beep 的大作中提到: 】
: 赞详细讲解
: 楼主的嵌入式场景,一般可能可维护性不是瓶颈,性能要求苛刻些,用union或transmute写成unsafe的c式内存操作应该也还是算合适的
:
--
修改:lvsoft FROM 180.111.26.*
FROM 180.111.26.*

【 在 dismoon 的大作中提到: 】
: 你这段代码的局限就是你还是认为我在发送一串“数据”
: 可能我的场景的确很小众
: 本来我是用一块Si24R连在UART上收发我一台机器的运行数据,现场工人就不用拆机器盖,连上线,导出数据了(场景特殊,不能在机器上开个口子做个USB或者其他接口,必须经常性密封)
: ...................
--
FROM 180.111.26.*
因为其实mcu场合用rust也挺舒服的。
国外在爱好者群体里这已经是个很显著的趋势了。
【 在 creek 的大作中提到: 】
: 赞。为啥在电路板讨论这么高层的rust,相对底层的电路而言?
:
--
FROM 180.111.26.*
mcu场合rust的工具链没啥不好的啊。我之前就介绍过,
比如st那个用c写的跟shit一样的hal库...跟rust对比就是天上地下。
只有具备rust这样强大的抽象能力之后,才有可能写出真正有价值的hal库。
比如你看看这里的example:
https://github.com/stm32-rs/stm32f4xx-hal/tree/master/examples
我贴个pwm的:
#![deny(unsafe_code)]
#![no_main]
#![no_std]
// Halt on panic
use panic_halt as _;
use cortex_m_rt::entry;
use stm32f4xx_hal::{
pac,
prelude::*,
timer::{Channel1, Channel2},
};
#[entry]
fn main() -> ! {
if let Some(dp) = pac::Peripherals::take() {
// Set up the system clock.
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let gpioa = dp.GPIOA.split();
let channels = (Channel1::new(gpioa.pa8), Channel2::new(gpioa.pa9));
let pwm = dp.TIM1.pwm_hz(channels, 20.kHz(), &clocks).split();
let (mut ch1, _ch2) = pwm;
let max_duty = ch1.get_max_duty();
ch1.set_duty(max_duty / 2);
ch1.enable();
}
loop {
cortex_m::asm::nop();
}
}
这才是写给人看的代码。
最后,如果你想用新的工具,享受它带来的好处,那首先是你得要用新的姿势去使用新工具。
比如给你一台联合收割机,你非说喜欢用镰刀亲自一茬茬割的感觉,用联合收割机又要加油又要维护启动半天就割一亩地是脱裤子放屁...那这个确实没啥办法,只能说你不需要联合收割机...
【 在 dismoon 的大作中提到: 】
: 最主要是MCU场合rust的工具链不好吧
: 我没说rust不能实现我的需求,我就是说,我本来好好的用一个强制double的地址赋值给char就能得到数据内存所在的指针,rust要绕一大圈脱几条裤子才能实现,unsafe就是那几条裤子
:
--
修改:lvsoft FROM 180.111.26.*
FROM 180.111.26.*
option result这些其实其他比较新的编程语言用多了也是很自然的。
不过,这些确实也是很容易被当成脱裤子放屁的地方...
我只能说,众多编程语言都演化成这种相似的模式不是没有道理的。
至于熟悉问题嘛,用多了自然就熟悉了,关键是要用起来。
【 在 beep 的大作中提到: 】
: again,不需要绕大圈脱裤子,lvsoft说的union可能麻烦些,我说的mem::transmute基本等同于c的写法,没啥问题。
: rust的门槛,第一在于对所有权和生命周期的理解,然后第二就在于对标准库的熟悉了,有时候得背标准库。mem下的一大堆东西,option result下的一大堆东西,你不熟悉就会觉得各种别扭
:
--
修改:lvsoft FROM 180.111.26.*
FROM 180.111.26.*
rust其中的一个目标是跟c同等地位,也即c能做的rust一定能做。
就说指针好了,mcu里的寄存器全部都是定义一串地址偏移量的指针。c里面都是一个头文件里面裸地址直接写在上面然后强制转换成指针。
rust当然也可以这么写,但给rust做hal库的人每这么做。他们的做法要安全并且严格的多。它利用rust的宏特性直接导入mcu厂方给出的svd文件。除非你去改svd,否则压根不存在改错的可能。当然,厂方也是会犯错的,所以这个导入svd文件的功能还有patch能力,可以按需要导入base svd,patch svd等等。
然后,因为是svd文件,所以rust对寄存器的操作可以精确到每一个bit的可读写性,以及humanreadable的可读性,和对每一个bit都有注释的完整文档。
比如同一个bit,读出来的值和写入的值的名字都是不一样的。比如读出来叫enabled,写进去得写enable,你如果写enabled直接就一根红线画上去提示你这里不对了。
至于安全特性对mcu是否有用,我只能说这就是嵌入式行业明明发展的时间和软件行业差不多久,应用规模也差不多大,但为啥生态规模相比之下小的可以忽略不计。就是因为大家都缺乏这种合作的思想,都满足于自己的一亩三分地,重复造自己的那个小轮子。自己在家做轮子,当然不需要上下游产业链配套了。而当你需要依赖生态的时候,安全特性就是相当重要的问题。
说到生态的问题,这里一个典型的案例就是arduino。刚出来的时候我也瞧不上arduino。但发展到现在arduino已经有了非常丰富的生态,现在很多时候,我自己都是更倾向于用arduino直接搞定的,这就是生态的威力。
【 在 Oriphia 的大作中提到: 】
: rust就不是给MCU用的,MCU最直接就是用显性指针,而不是隐性指针,安全特性对于MCU完全没有用。
:
--
修改:lvsoft FROM 180.111.26.*
FROM 180.111.26.*
都说了好多遍了,rust和c在指针方面没任何区别。
你要把地址cast成指针,最简单比如这样就行了:
static mut SCREEN: *mut [[u16; 80]; 25] = 0xB8000 as _;
严谨一点可以封装下,比如这个看起来复杂是因为还定义了生命周期:
fn from_addr<'b>(address: usize) -> &'b Name<'a> {
unsafe { &*(address as *const Self) }
}
至于java也有指针...你是认真的么?rust和java的重大区别就是rust和c一样直接面向实际硬件体系架构。而java多了一层jvm。java别说直接访问内存了,连off heap这么naive的事情都可以当成黑科技。
最后,所有语言都是等价的,你当然可以在c层面做到和rust一样只输入svd来定义整个寄存器的内存映射。但如果不依赖meta programming,只靠c的macro,肯定做不到zero cost。而依赖meta programming,那我随便拿什么语言都行,比如我用python都是可以的。这本质上就是回到了c++的错误路线上(把一个template整成图灵完备,which我认为是个十分sb的决策)
以外,尽管语言都是等价的,但不同的语言的style很重要。我们选择一门语言,选的就是它的style,也即它在设计之初定下的优点/缺点组合的倾向性。比如rust的stm-hal,人家选择直接导入svd,而不是c style式的写个头文件嵌入进去。就是因为选择rust的人会更看重安全性,所以他们也会天然的选择更安全的实现方案。这些东西比各种具体的feature,诸如指针是否直观易用写起来是否方便什么的要重要得多。
最后,还是回到我前面的观点,直观易用=简陋=镰刀割麦子。当你想要追求生产力,开始追求现代化工具带来的高效率的同时,你就必须抛弃掉刀耕火种的手工风格。简单易用(C style)和复杂完美(C++ style)都不是优点,作为工程师,能否做出恰到好处的取舍,选中最完美的那个平衡点,才是定义一个好的工程师的标准。
【 在 Oriphia 的大作中提到: 】
: C的显性指针明显更加直观易用,如果rust算有指针,那JAVA也有指针,这些隐性的指针对寄存器操作没有何任的易用性可言,如果我没记错,ESP32的SVD文件就有高达4万行,本质上是一本字典。我觉得在C语言环境做这种字典也是可以的,但既然C有显性的指针,svd就变成画蛇添足。
: 安全特性对于多任务系统中,不同程序之间的内存管理的确更安全,问题MCU的ROM本身只是一个程序,MCU的程序开发多数情况又是一个人完成。安全性就相当于偷窃我的人是我自己,没有半点意义。
:
--
FROM 180.111.26.*
基本上可以认为和c差不多吧。
当然这里所谓的差不多是指0.5-2倍的波动。
包括性能,代码密度,运行时内存占用等各方面指标。
这些其实跟不同的版本,不同的编译器,不同的编译参数都有关系。
具体在嵌入式场合,rust因为有generic特性,不注意的话容易因为一个不起眼的依赖关系最后牵扯出整座森林。
但这种问题c也一样,比如当年我刚写51的时候不小心随手写了个浮点数,结果编译就包含了整个软件浮点库。
关于rust和c的各方面对比,这个blog写的比较全面:
https://kornel.ski/rust-c-speed
【 在 spadger 的大作中提到: 】
: rust代码密度如何?
:
--
FROM 180.111.26.*