< 返回版块

kipade 发表于 2025-03-10 11:11

Tags:c_char, String

大家好,我新开始学习rust语言一周,以前是个C程序员,现在有个很大的疑问: use std::os::raw::c_char; struct A { i:u32, s:[c_char:32] }; 我遇到了实例化的疑问:

  1. 如果我直接声明变量 let a = A{ i:0, s : [这里需要写32个c_char才行]}; 而事实上,大多数情况下,我不需要32个字符,所以这种方法好像不行
  2. 构造函数: 我希望能使用 let a = A::new(10, "abc");这样的语句来构造 impl A{ pub new(ii:u32, ss:&str) -> Self { //我发现,这里是不是需要先有一个临时的c_char数组,或者是CString之类的变量,先从sst复制进来,假定为ss2 //然后 A{ i: ii, s: ss}; //这里是不是又引入了一次复制 } 第二种方案是否能够优化掉一次复制? 谢谢

评论区

写评论
作者 kipade 2025-03-11 10:36

这是我第二次尝试应该得到的答案,够简单直接。谢谢

作者 kipade 2025-03-11 10:35

真是不能太详细了,我也都看明白了,接下来我得好好看看你推荐的这些个资料,其中那个Atomics_and_Locks我已经看过了。非常感谢

ThalliMega 2025-03-10 23:06
use std::ffi::CStr;

#[derive(Debug)]
#[repr(C)]
struct A {
    i: u32,
    s: [u8; 32],
}

impl A {
    pub fn new(i: u32, s: &CStr) -> Self {
        let mut a = A { i, s: [0; 32] };
        let sb = s.to_bytes_with_nul();
        a.s[..sb.len()].copy_from_slice(sb);
        a
    }
}

fn main() {
    let a = A::new(114514, c"1919810");
    println!("{a:?}")
}
TinusgragLin 2025-03-10 17:32

通过你的建议,我想我写出来了,像下边这样

哈哈哈,可能我说的不是很清楚,我的意思是可以这样:

pub fn new_(ii: u32, ss: &str) -> Self {
    // 将输入的 &str(底层是 &[u8])转成 &[i8]
    let s: &[i8] = unsafe { std::slice::from_raw_parts(ss.as_ptr().cast(), ss.len()) };
    // 零初始化一个新数组
    let mut tmp_arr: [i8; 32] = [0; 32];
    // 复制
    tmp_arr[..s.len()].copy_from_slice(s);
    // 赋值(“移动”,语义上来说是按字节复制,实际上不会)
    Self { i: ii, s: tmp_arr }
}

或者这样:

pub fn new(ii: u32, ss: &str) -> Self {
    // 拿到 &str 底层的 &[u8]
    let bytes: &[u8] = ss.as_bytes();
    // 零初始化一个新数组
    let mut tmp_arr: [i8; 32] = [0; 32];
    // 依次复制(会被优化为 memcpy)
    for i in 0..ss.len() {
        tmp_arr[i] = bytes[i] as i8
    }
    Self { i: ii, s: tmp_arr }
}

我这还是有2次复制,我希望能把第一次复制干掉,可以原地构造复制

第一次是零初始化,因为对于基本上所有的类型,Rust 都要求在使用实例前进行初始化,即使是不管是什么值都是有效的整数类型,要不然编译器就可能会进行错误的代码优化,要绕过这个限制就需要 MaybeUninit 和复杂一点的代码:

pub fn new(ii: u32, ss: &str) -> Self {
    // 确保输入不含 \0
    assert!(!ss.contains('\0'));
    // 拿到 &str 底层的 &[u8]
    let bytes: &[u8] = ss.as_bytes();
    let src_len = bytes.len();
    assert!(src_len < 32);

    // 来一个未初始化的 [i8; 32]
    let mut uninit_arr: MaybeUninit<[i8; 32]> = MaybeUninit::uninit();
    let inited = unsafe {
        // 拿到指向数组头部的指针
        let ptr = uninit_arr.as_mut_ptr() as *mut i8;
        // 0 到 src_len 复制输入
        ptr.copy_from(bytes.as_ptr().cast(), src_len);
        // src_len 到 32 填 0
        ptr.add(src_len).write_bytes(0, 32 - src_len);
        // 告诉类型系统 uninit_arr 里面的 [i8; 32] 已经完全初始化好了
        uninit_arr.assume_init()
    };

    Self { i: ii, s: inited }
}

由于第二次复制,a被声明为mut, 如此返回,虽然能过编译,为什么就没有属性丢失或者冲突?

a 被初始化(ii=0, s=[0;32])后,a.s 被修改,修改之后再返回,不知道你疑惑的具体是什么?

你看了一下,这一下是在哪儿看到的?

哈哈,就是跳到定义啦。

如果有,谁负责释放?是否真的会被自动释放?

Box 这个类型意思就是,我会在堆上申请足够的空间,然后放入 T,如果我没有被移动(赋值)到其他地方的话,我会在块 ({ ...... }) 结尾处自动释放申请的空间,如果你可以看一下输出的汇编,就会看到编译器在合适的位置自动插入的 std::ptr::drop_in_place。

谢谢回复,我在w3cschool上看的教程。你发的这个嘛,肯定也没问题,但是它是鸟语啊

确实,这个网站可能了解的人不是很多,还没有人贡献多语言支持。我的浏览器一直有谷歌翻译插件,翻译效果还过得去,不过我之前听说彩云小译和 DeepL 这两个翻译效果更好,你可以试试!

已经有中文翻译的书的话,在线上的有Rust 程序设计语言Rust 语言圣经 、Google 的 Comprehensive RustRust 秘典Rust 原子变量和锁Effective Rust 这些,实体书的话,官方的入门书已有翻译《Rust权威指南》,也有很多人推荐 《Rust程序设计》

你的原问题涉及 C ffi 和 unsafe Rust,是比较进阶的知识了,如果你已经看完了官方的那本入门书,就可以看《Rust 秘典》了,这是官方的介绍 unsafe Rust 的进阶书。

artiga033 2025-03-10 17:02

当然是指已知输入的情况,即字符串字面量(&'static str),因为在C语言中, char[] 本质是指针,可以直接指向静态内存的地址。然而Rust的数组是Owned的,无论如何都必须复制一份数据,除非换用&mut [c_char;32]。而且事实上,如果你在写ffi与C交互的话,后者才更像C语言的数组,当然放在结构体里的话,内存布局就不对了,所以如果追求性能还是得unsafe。

--
👇
kipade: 如何提前到编译期?恐怕有问题吧,运行时的输入无法在编译期得知

作者 kipade 2025-03-10 15:35

如何提前到编译期?恐怕有问题吧,运行时的输入无法在编译期得知

作者 kipade 2025-03-10 15:27

通过你的建议,我想我写出来了,像下边这样: pub fn new(ii:u32, ss: &str) -> Self { let s: &[i8] = unsafe { std::slice::from_raw_parts(ss.as_ptr().cast(), ss.len()) }; let mut a = A {i:ii, s:[0;32]}; //第一次复制 let mut i = 0; for c in s { //第二次复制 a.s[i] = *c; i = i+1; } a } 那我有两个问题:

  1. 我这还是有2次复制,我希望能把第一次复制干掉,可以原地构造复制
  2. 由于第二次复制,a被声明为mut, 如此返回,虽然能过编译,为什么就没有属性丢失或者冲突?

另外就是,你提到“我看了一下 CString 里面是一个 Box<[u8]>,感觉会引来不必要的内存申请。”,你看了一下,这一下是在哪儿看到的?然后,感觉会带来不必要的内存申请,如果有,谁负责释放?是否真的会被自动释放?

TinusgragLin 2025-03-10 13:27

如果你已经过了《Rust 编程语言》,推荐这个网站,里面有很多常用类型的内存布局,我感觉对从 C 这样的底层语言过来的朋友会很有帮助!

TinusgragLin 2025-03-10 13:16

let a = A{ i:0, s : [这里需要写32个c_char才行]};

c_char 就是 i8,如果是零初始化的话,可以用 [0; 32]

先有一个临时的 c_char数组,...,先从 ss 复制进来

注意 Rust 中一个 &str 中间是可以有 \0 的,可以先检查一下。

&str 底层是一个 &[u8] (可以用 .as_bytes() 获取到),但是 c_chari8,可能需要来个 unsafe

let s: &[i8] = unsafe { std::slice::from_raw_parts(ss.as_ptr().cast(), ss.len()) };

再用 tmp_arr[..s.len()].copy_from_slice(s) 复制。不想用 unsafe 的话也可以一个一个复制,虽然看起来有点蠢,但是编译器非常聪明,会直接把这个循环优化成 memcopy

或者是 CString 之类的变量

我看了一下 CString 里面是一个 Box<[u8]>,感觉会引来不必要的内存申请。

A{ i: ii, s: ss}; //这里是不是又引入了一次复制

语义上来说是有一次复制,但是嘛,编译器非常聪明!

artiga033 2025-03-10 12:58

Rust 中的 String 与 C 的char* 有很大不同,其一就是 String 必定是在堆上的。而C的数组本质是指针,且也不区分堆栈内存。

所以对于你的方法2,可以写一个const fn来从str转换到c_char数组,这样一部分运算会提前到编译期。但是复制是不可避免的,即使是在C中那也是因为指针可以乱指,而这经常引发内存安全问题(可以用unsafe做到类似的事,但是unsafe)。

easyfree 2025-03-10 12:16

先定义一个临时数组变量,然后在用数组构造结构体,一般来讲编译器会直接优化掉临时变量的移动(或复制),就相当于是直接在A结构体分配的内存上直接初始化的

但是临时数组[c_char;32]的完整初始化和从&str[c_char;32]的复制是没法避免的

1 共 11 条评论, 1 页