I hate to admit this, but it took me a long time to do, mostly because I was not experienced when I started writing code, and the code has been rewritten several times over to get it right.
First off is the code in [1]. It's technically under several licenses, but I can give it to you under the public domain because bounds checking in C is important enough that I'll give it away.
The `y_ARRAY_TYPE()` macro generates a struct that is an array with bounds. Use it like this:
typedef y_ARRAY_TYPE(uint32_t) uint32_t_arr;
`uint32_t_arr` is a bounded array of `uint32_t`'s.
You can then pass that array to functions and do bounds checks with the `y_i()` macro (for when the struct value is not a pointer) and the `y_ip()` macro (for when the struct value is a pointer).
To implement RAII, the code to do that is in the same repo, but it's...much more complicated: a full stack allocator. I just add the destructor as a parameter when allocating memory, and when the allocation is freed, the destructor is called when it's non-NULL. Then there are functions to allocate and free, but it is always done in a stack, to emulate the real stack.
By the way, the type of destructor is
void (*y_Destructor)(void*)
and it takes a pointer to the item to be destroyed.
Structured concurrency takes the idea of a thread-local stack and sort of generalizes it across threads. The idea is that each thread has a parent, and that parent is not joined until all of its child threads are joined. All threads form a tree, almost exactly like process trees in Linux.
This means that if you want to have another thread borrow an item, you just have to make sure that thread is a descendant of the thread it is borrowing the item from, and you have a guarantee that the item will never go out of scope before the borrowing thread does.
Sometimes you need to change your code to do that. You can "push" things "down the stack" by passing function pointer callbacks to callees who then create the item to borrow and call the callbacks. Inside the callbacks, the borrowed item will always exist.
There's more to it than that (you can turn a thread tree into a DAG [2], allowing you to turn threads that are not descendants of a thread into descendants), but that's probably good enough to start you off.
Dereferencing NULL pointers: my code is structured such that every function parameter that can be NULL is marked so. And I check those possibly-NULL pointers.
This could also be done with a macro:
#define y_d(p) ((p) == NULL ? abort(), *p : *p)
Otherwise, the compiler (clang, usually) warns me when a NULL pointer is passed in and I have asserts to catch them.
Even before I had all of this infrastructure, deferencing NULL pointers was not a problem for me personally; it's just one of the mistakes I tend to not make. So that's why I don't have a macro for it.
For use-after-free, I use scopes. My stack allocator has a function that will mark the start of a scope, and I call it when entering a new scope. Then the pointers that are allocated during that scope will all be freed when I call the function to free every item in the scope, but those pointers will also go out of scope, which means the compiler won't let me access them.
It looks like this:
void
func(size_t s)
{
y_stackallocator* p = y_strucon_stackallocator();
// Random code.
// Open a new scope. Notice that the scope is a bare scope.
// But it could also be an if statement or a loop.
{
size_t* sp = y_stackallocator_malloc(p, sizeof(size_t) * s, NULL);
// Random code.
// This frees everything in the scope, including sp.
y_stackallocator_scopeExit(p);
}
// Scope has exited; sp is not accessible, no use-after-free.
}
This is less rigorous than Rust, but I use the principles in Joel's "Making Wrong Code Look Wrong" [1] here. If I have scopes that don't look like the above, they are wrong.
I get where you're coming from - there are plenty of parts of C++ that I dislike also, but this kind of 'clean up on scope exit' has always been one of C++'s strengths - even before modern c++, and is significantly less error-prone than the solution you present above.
Do you have a tool that enforces the use of all of these macros and stuff? How do you know that, for instance, you didn't forget something? Seems like one slip-up is all it would take.
This is where C fails against Rust. I have to use static analyzers to find such problems.
But I do use them, and I also use sanitizers against large and thorough test suites, with excessive fuzzing thrown in for good measure. And I mix all of that with crash-happy code littered with gobs of `assert()` calls that document as many of my assumptions as I can find.
Those help me find about all I can find.
But I'm still writing a language that is as safe as Rust that I will auto-translate my code into when it's done.
By the way, I'm not using Rust because I don't like it. That doesn't mean it's not good, but I really hate async/await, along with a few other Rust design decisions.
I'm kind of picky as a programmer.
Yes, C fails against Rust here, but that's why I put in the extra effort to bring it up to the same level regardless.
> But I'm still writing a language that is as safe as Rust that I will auto-translate my code into when it's done.
Just curious, how do you intend to make it memory safe? By using a garbage collector, automatic reference counting, a borrow checker, or something else?
By structuring the language around RAII and structured concurrency. More details in my great-great-great-great-grandparent post. [1]
tl;dr: If threads never exit before their descendants, and you only borrow an item in callees and in descendant threads, that would negate the need for a borrow a checker.
As for sharing an item across threads and still not having data races, this will be done by implementing something close to Rust's Send/Sync.
There will be ARC implemented by RAII, just like Rust.
Basically, the language will be built around structured concurrency, including the standard library.
Ah, sorry, my bad, I should have read the whole thread in detail first instead of just skimming it. (:
So do you also intend to completely disallow mutation? Because that's the only way I could see this matching Rust's safety guarantees without actually having a borrow checker.
Say, for example, that you allocate a new string, and you take a reference to it, and then you append to that string within the same scope. This triggers a reallocation which will make that reference invalid. To make that safe you'd either need to have a borrow checker (to be able to detect at compile time that the reference was invalidated), or you'd have to disallow mutation (the act of mutating wouldn't actually mutate that string, but create a new one, leaving the old one alone until it goes out of scope).
Mutation will be disallowed in general, yes. It's a bit more subtle than that, but you get the gist.
In my language, there are strings, which are fixed size and cannot be reallocated, and string builders, which hide the actual string behind another layer of indirection to avoid the problem with mutation.
That sort of pattern would be used as necessary to avoid the problems with mutation in the same thread. And Send/Sync would take care of the rest across threads.
I have macros for that that I use instead of the keywords. If the keywords appear in code that uses scopes like that, it's a bug.
I still have a lot of technical debt with that, but I haven't gotten to it. However, when I do change it, I'll be able to find all instances automatically with grep.
There's a compiler extension to tag variables with a function called on scope exit. Attribute cleanup. For dubious portability reasons I'm unwilling to rely on it for correctness but it's really useful in a debug build for types where you can detect they haven't been cleaned up.
Change MyType to RAIIMyType or similar in the function to get the checking when using GCC/clang with asserts enabled. In practice sometimes types should drop out of scope without deallocation - return from functions etc. Thinking about it now I should probably apply it by default and use (MyType) to avoid the macro expansion selectively.
Let's be honest, `y_i(array, 10)` is much less readable than `array[10]`. Sure, you can program with bounds checking, but it's less readable, and less newbie-friendly.
First off is the code in [1]. It's technically under several licenses, but I can give it to you under the public domain because bounds checking in C is important enough that I'll give it away.
The `y_ARRAY_TYPE()` macro generates a struct that is an array with bounds. Use it like this:
`uint32_t_arr` is a bounded array of `uint32_t`'s.You can then pass that array to functions and do bounds checks with the `y_i()` macro (for when the struct value is not a pointer) and the `y_ip()` macro (for when the struct value is a pointer).
To implement RAII, the code to do that is in the same repo, but it's...much more complicated: a full stack allocator. I just add the destructor as a parameter when allocating memory, and when the allocation is freed, the destructor is called when it's non-NULL. Then there are functions to allocate and free, but it is always done in a stack, to emulate the real stack.
By the way, the type of destructor is
and it takes a pointer to the item to be destroyed.Structured concurrency takes the idea of a thread-local stack and sort of generalizes it across threads. The idea is that each thread has a parent, and that parent is not joined until all of its child threads are joined. All threads form a tree, almost exactly like process trees in Linux.
This means that if you want to have another thread borrow an item, you just have to make sure that thread is a descendant of the thread it is borrowing the item from, and you have a guarantee that the item will never go out of scope before the borrowing thread does.
Sometimes you need to change your code to do that. You can "push" things "down the stack" by passing function pointer callbacks to callees who then create the item to borrow and call the callbacks. Inside the callbacks, the borrowed item will always exist.
There's more to it than that (you can turn a thread tree into a DAG [2], allowing you to turn threads that are not descendants of a thread into descendants), but that's probably good enough to start you off.
[1]: https://git.yzena.com/Yzena/Yc/src/commit/cf4c96b3560d/inclu...
[2]: https://lobste.rs/s/8msejg/notes_on_structured_concurrency_g...