Your function is easy to write in (unsafe!) Rust:
https://play.rust-lang.org/?version=stable&mode=debug&editio...
use lazy_static::lazy_static; // 1.4.0
use std::mem;
lazy_static! {
static ref BUF: [String; 16] = [
String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024),
String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024),
String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024),
String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024), String::with_capacity(1024)
];
static ref BUF_INDEX: usize = 0;
}
fn tmpstr() -> &'static mut String {
let index_ptr: * const usize = &*BUF_INDEX;
let index: &'static mut usize = unsafe { mem::transmute(index_ptr) };
let buf_ptr: * const String = &BUF[*index];
let ret: &'static mut String = unsafe { mem::transmute(buf_ptr) };
ret.truncate(0);
*index = (*index + 1usize) % BUF.len();
return ret;
}
fn main(){
let s1 = tmpstr();
s1.push_str("Hello, world!");
let s2 = tmpstr();
s2.push_str("And hello world again!");
dbg!(s1, s2);
}
BTW: It's very good idea for embedded systems.The difference between Rust and C in terms of memory model isn't ownership, it's “mutable xor shared”.
It's possible to use pointers to circumvent the restriction and then control when object is destructed using ManuallyDrop[0], so burden of memory bookkeeping will be shifted from Rust compiler to the programmer.
[0]: https://doc.rust-lang.org/core/mem/struct.ManuallyDrop.html
Can you just declare a static array and return a pointer pointing to any part of it? Like in the C example?
BUF is also visible to other parts of the program, correct? If so, it's not quite similar to the C example.
Indeed:
lazy_static! {
static ref BUF: [[u8;1024]; 16] = [
[0;1024], [0;1024], [0;1024], [0;1024],
[0;1024], [0;1024], [0;1024], [0;1024],
[0;1024], [0;1024], [0;1024], [0;1024],
[0;1024], [0;1024], [0;1024], [0;1024]];
static ref BUF_INDEX: usize = 0;
}
(You'll need to re-implement a `push_str` method on arrays yourself though)> BUF is also visible to other parts of the program, correct? If so, it's not quite similar to the C example.
No, because it's not marked as `pub`, nobody outside the current module can access it.
Yes, of course. BTW: You can use a c2rust converter to convert a C code to it exact equivalent in Rust.
No, buf is not exported, so it's "static" in C terminology.
Yes, I know that is not exported. What I wanted to say is that in the C example, no one outside tmpstr(void) can access static char buf[16][1024].
In the Rust example I can access static ref BUF from fn main() function, for example.
Only because in that simple example, BUF was defined directly in the main module.
In a more realistic exemple, BUF and tmpstr would belong to their own module, with only tmpstr being exported (and then accessible). See: https://play.rust-lang.org/?version=stable&mode=debug&editio...
Because C does not have lifetimes at all, or ownership as a formal langage concept, and thus does not run head first into the completeness problem.
Which is one of unsafe’s use cases: when the compiler can’t prove the code is correct (because there will always be correct programs the compiler can’t prove, and there are currently a fair bit of them), and you can, and there are no suitable safe alternatives, you use unsafe.
> For example: a common pattern in video games is the frame-local allocation. You (cheaply) allocate some object at the beginning of a frame. Share references to it all over the place. At the end of the frame, you (cheaply) free all the objects that were allocated in this manner. This cannot really be expressed in terms of rust's ownership system.
Of course it can, though it requires APIs which are not there (in the stdlib, yet): a frame local allocator and custom per-object allocator. All frame-local objects use that and thus keep a reference to the allocator.
And it avoids storing frame-local objects in locations which outlive the frame.
> This function is not locally correct; in a vacuum, it is impossible to say whether this function violates invariants.
That is also a use-case for unsafe: an unsafe function is one for which the caller is responsible for checking invariants.
This is unsafe anyway because modifying globals is unsafe to start with.
> Of course it can
You cannot share objects using rust's ownership system.
Interior mutability involves an unacceptable performance hit (and doesn't actually allow you to share anything). And if you use shared indices into a centralized array, then you have blinded the ownership system, creating pointers that it cannot make sense of.
>> This function is not locally correct; in a vacuum, it is impossible to say whether this function violates invariants.
> That is also a use-case for unsafe: an unsafe function is one for which the caller is responsible for checking invariants.
That's exactly right, and was my thesis; rust's type system does not allow you to prove the correctness of such a function, because it is neither correct nor incorrect without context. 'Unsafe', as a subversion of the type system which allows unsound behaviour, is completely uninteresting wrt the topic at hand; the comparison is a purely theoretical one between non-'unsafe' rust—that is, rust's ownership model—and c.
Unsafe does not allow unsound behavior, or rather, it lets you write unsound behavior but that is a bug. You still have to uphold various invariants in unsafe code. It’s there for when you know you can, but the compiler cannot, which is an inherent property of any sound system.
I don't understand what you're saying here.
Rust-without-'unsafe' is a sound language; i.e. any program which types will be unable to violate runtime invariants.
Rust-with-'unsafe' is an unsound language; i.e. there are programs which type but yet violate runtime invariants.
That is a bug in those programs—sure, yes, of course. It is not a bug in rust; it is the language functioning as intended. However the topic at hand is the relationship between rust's type system and c's; hence unsafe is completely uninteresting, because it is a tool for subverting rust's type system.
(It occurs to me now that I might have gotten a less generally defensive response had I made the original comparison between rust and a hypothetical rust without unsafe. Oh well)
Gotcha, I was just trying to clarify the semantics of unsafe, lots of people think it suddenly lets you do whatever you want, and while it will let you, that doesn't mean that you can, which is an important distinction. You're right that this puts it in the same general category of C in a theoretical perspective, I think practice will bear out that it's not a binary proposition, but time will tell :)
C is like a laser gun in a room full of oddly-shaped mirrors with no safety at all. Good luck.
That's what type systems in general do. There is no type system for a turing-complete language that is both sound and complete. (Trivial proof: if (undecidable) halt; unsound.)
For the record, the “topic at hand” was the memory model not the type-system, nor the theorem proving capabilities of the borrow checker.
Your arguments are not wrong per se, but they are completely missing the topic.
So here's another, hopefully better expression:
Rust's type system wrt lifetimes is not very expressive, compared with 'dynamically-typed' c. It can't represent things like 'the value returned by this function must remain alive for no more than 15 successive invocations of this function'.
For example: a common pattern in video games is the frame-local allocation. You (cheaply) allocate some object at the beginning of a frame. Share references to it all over the place. At the end of the frame, you (cheaply) free all the objects that were allocated in this manner. This cannot really be expressed in terms of rust's ownership system.
More generally, rust enforces local correctness, while c allows for global correctness (a weaker condition). Consider, as a simpler example, this c function which I write frequently:
This function is not locally correct; in a vacuum, it is impossible to say whether this function violates invariants. (Ignoring the problem of overflowing the 1024-byte buffer.) There are many programs using this function which are correct; then, there are some programs which call this function more than 16 times in a row and end up with undesirable behaviour as a result.Rust requires that every function be locally correct. Papering over some details: for any sound function f and hole-containing program P[_], if P[f] is well-typed then P[f] is sound; that means that it's always meaningful to consider f without considering the context in which it will be called. There is no such guarantee in c.
As a sound language, this is a very useful attribute for rust to have, because it means that libraries can be meaningfully type-checked (not to mention it significantly simplifies the type system), but it would be naive to say that rust simply formalizes the ownership model you would have used anyway in c.