I worked with both Ada and with Rust quite a bit. There's a lot of features that both have, but Rust comes out ahead as being a "safe" language. Ada is not fully memory safe. Yes, you can corrupt memory in Ada, and even have dangling pointers but it's much harder to do than in C.
Rust constricts all the scary ops to
unsafe blocks where it's expected that you know what you're doing and you make a safe interface to hook it into the rest of your code. You're not allowed to just go play around in memory and store pointers to random spots in memory and hop around like a frog with pointer arithmetic in normal Rust code.
Both Ada and Rust provide bounds checking of arrays.
Ada's built-in strings aren't actually null-terminated, they're just a bounds checked
array of Character.
Ada allows returning variable-length arrays out of functions since arrays carry their own bounds.
This means a lot of times you won't heap allocate where you otherwise would in other languages, and there's a lot fewer sharp corners because your indices get checked. There's a built-in
Storage_Error, which gets raised in this case on stack overflow.
Ada doesn't have plain pointers, instead there's these other things called
access types, which are a form of typed pointer. Access types obey a concept called "accessibility" (unrelated to a11y) which only allows them to only point to variables which live as long as they do, in a similar, but less overall complete way than how borrowing works. This prevents pointing to something in a scope which can disappear on you.
In addition, accesses also get checked at runtime to see if they are null, this penalty can be removed if marked as
not null (sort of like a C++ reference) or it's known to always have a value. You cannot perform pointer arithmetic on access types.
If you really want to do this, you can dive into
Interfaces.C and convert to a system address if you want, but that's usually a red flag.
Each access type has its own storage pool (allocator) and you allocate using
new to grab something and
Unchecked_Deallocation to free it, which also nulls out the access (unlike C). Freeing an object and then accessing it again will trigger a runtime exception, as will running out of memory when trying to allocate.
This system works pretty well in general for dealing with dynamic allocation.
Rust uses referenced-counted types, Rc and Arc, as well as a move-only Box for this.
Since Ada has it's own version of drop (i.e. RAII) as well as "immovable (
limited) types", you should be able to make Ada equivalents now that it has a package manager like cargo, and there's already a version of Rc in one of the major libraries. However, you'd have to use the method discussed down below to control concurrent reads/writes in Ada, whereas in Rust the compiler will ensure you don't shoot yourself in the foot.
Pointers (access types) don't get used nearly as much use in Ada as they do in C or C++. Parameters to functions are immutable by default, unless they're indicated as being written to, an
out parameter. There's some other rules about specific things being passed by reference --those with virtual function tables, and immovable (called "limited") objects.
In addition to locking access types to its own storage pool, there's another type of access which is
access all, and can access any variable of its type, but only if it is marked as
aliased. This helps limit down the cases of where accesses get used, but would be the main source of memory problems.
Rust's borrow checker only lets you lend out a single mutable reference to a variable at once. If you hand a mutable reference to a variable to a thread, the compiler prevents you from handing out another one.
Your other options are handing out mutexes instead which will let you interleave writes, or you can use message passing. (EDIT: It's been a while since I did this, help me ensure I'm still accurate).
For shared mutable state between multiple threads, Ada uses
These are sort of like a struct with functions which can only return values, and procedures which can read/write. This allows concurrent read access, while procedures prevent writes during reads. You can also define special function-like things called entries which are like a procedure but have a conditional guard called a "barrier". Barriers can be arbitrary conditions and get checked at the end of an entry or protected procedure to enable a waiting thread to proceed. Queueing gets handled by the language and there are tunable parameters via pragmas.
EEC GDPR compliant