0

I need to read a file into a null terminated string at compile time.

Working in Rust OpenGL. I have a shader source code stored in a separate file. The function that will eventually read the source is gl::ShaderSource from the gl crate. All it needs is a pointer to a null terminated string (the std::ffi::CStr type).

Typically the guides I have seen read the shader source file using include_str!, then at run time allocate a whole new buffer of length +1, then copy the original source into the new buffer and put the terminating 0 at the end. I'd like to avoid all that redundant allocating and copying and just have the correctly null terminated string at compile time.

I realize it is somewhat petty to want to avoid an extra allocation for a short shader file, but the principle could apply to many other types of larger constants.


While scrolling through suggested questions during the preview I saw this: How do I expose a compile time generated static C string through FFI?

which led me to this solution:

    let bytes1 = concat!(include_str!("triangle.vertex_shader"), "\0");
    let bytes2 = bytes1.as_bytes();
    let bytes3 = unsafe {
        CStr::from_bytes_with_nul_unchecked(bytes2)
    };
    println!("{:?}", bytes3);

Does this accomplish avoiding the runtime allocation and copying?

DanielV
  • 643
  • 4
  • 14

2 Answers2

4

Your code is unsound. It fails to verify there are no interior NUL bytes.

You can use the following function to validate the string (at compile time, with no runtime cost):

pub const fn to_cstr(s: &str) -> &CStr {
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < (bytes.len() - 1) {
        assert!(bytes[i] != b'\0', "interior byte cannot be NUL");
        i += 1;
    }
    assert!(bytes[bytes.len() - 1] == b'\0', "last byte must be NUL");
    // SAFETY: We verified there are no interior NULs and that the string ends with NUL.
    unsafe { CStr::from_bytes_with_nul_unchecked(bytes) }
}

Wrap it in a little nice macro:

macro_rules! include_cstr {
    ( $path:literal $(,)? ) => {{
        // Use a constant to force the verification to run at compile time.
        const VALUE: &'static ::core::ffi::CStr = $crate::to_cstr(concat!(include_str!($path), "\0"));
        VALUE
    }};
}

Then use it:

let bytes = include_cstr!("triangle.vertex_shader");

If there are interior NUL bytes the code will fail to compile.

When CStr::from_bytes_with_nul() becomes const-stable, you will be able to replace to_cstr() with it.

Chayim Friedman
  • 47,971
  • 5
  • 48
  • 77
-2

Yes that should work as intended. If you want you can even bundle it into a simple macro.

macro_rules! include_cstr {
    ($file:expr) => {{
        // Create as explicit constant to force from_bytes_with_nul_unchecked to
        // perform compile time saftey checks.
        const CSTR: &'static ::std::ffi::CStr = unsafe {
            let input = concat!($file, "\0");
            ::std::ffi::CStr::from_bytes_with_nul_unchecked(input.as_bytes())
        };
        
        CSTR
    }};
}

const VERTEX_SHADER: &'static CStr = include_cstr!("shaders/vert.glsl");
const FRAGMENT_SHADER: &'static CStr = include_cstr!("shaders/frag.glsl");
Locke
  • 7,626
  • 2
  • 21
  • 41
  • It would have been better to not spread the knowledge by adding to the duplicate question instead of here. – cafce25 Jan 10 '23 at 01:13
  • 2
    No this does not work as intended, this is unsound. – Chayim Friedman Jan 10 '23 at 05:02
  • 1
    @ChayimFriedman Unsound is the wrong term, I think you mean error prone – DanielV Jan 10 '23 at 05:55
  • 1
    @ChayimFriedman that is not fully correct. `from_bytes_with_nul_unchecked` does validate inputs, but only when used in a const context. In this way, the call is always unchecked at runtime, but it can ensure that the input upholds the safety contract when possible to do so at no cost to the user. That being said, I had written it in a way where it could be evaluated in a non-const context in some cases, but that was easily fixed by creating a constant in the macro. However, I would also understand some hesitancy in relying upon `const_eval_select` since it is not well documented yet. – Locke Jan 10 '23 at 06:00
  • 1
    @DanielV No, I meant unsound, as it can be used to cause UB. – Chayim Friedman Jan 10 '23 at 06:09
  • 1
    @Locke It is not documented, it can be removed without warning, and relying on it is plain wrong. – Chayim Friedman Jan 10 '23 at 06:09
  • 1
    @ChayimFriedman Worrying that error checking code would be removed just because it isn't in documentation is unrealistic. A valid criticism though is that the error checking code relies on the optimizer figuring out something can be computed at compile time, and applying the checks, which it doesn't. Also undefined and unsound are mutually exclusive, this is logic 101, for something to be unsound it has to be defined to contract a something. 1+1=3 is unsound, 1+1=? is undefined. This is neither of those, it is possible to trace exactly what `from_bytes_with_nul_unchecked` will do. – DanielV Jan 10 '23 at 18:07
  • 1
    @DanielV Worrying that error checking will be removed if it is not documented is the right thing to do. If it is not documented, it is like it doesn't exist. And I can imagine at least one possible reason for it to be removed - if somebody complains that because of this check they are unable to compile their big const string because it is too slow/above the const eval limit. And I meant unsound in the Rust term, where unsound is used for an API that is not marked unsafe and can be used to trigger undefined behavior. This macro fits this description perfectly. – Chayim Friedman Jan 10 '23 at 19:21
  • @DanielV Sorry, I don't understand your first comment (starting with "It cannot currently be used in a const context"). – Chayim Friedman Jan 10 '23 at 19:24
  • @DanielV If you think this guarantee is important, you can file an API change proposal or even just a pull request to the https://github.com/rust-lang/rust repo to include this guarantee in the docs. If such change will be made (I will be against it, but I don't decide on those things), I will agree this answer will become correct, although it will continue to be incorrect when written. – Chayim Friedman Jan 10 '23 at 19:26
  • @DanielV Also, as written, this doesn't rely on the optimizer at all. All checks are guaranteed to run at compile time (although this wasn't the case in the first revision of this answer). – Chayim Friedman Jan 10 '23 at 19:28
  • 1
    @DanielV I downvoted (and probably others) because this is unsound, which should be avoided at all costs. – Chayim Friedman Jan 11 '23 at 02:03