I also know of a few game development studios with R&D departments who have had an eye on Rust in the past, though I bet it will be a long time before a major studio is willing to make the risk that writing an engine in Rust would entail.
I know it's not your point, just providing some answers in case someone is wondering.
Sticking with C/C++ also means you'll be able to more easily port your game to any platform of the future. It's just a safe bet that any future platform (either hardware or software) will support C/C++. Especially for games. Anything else is a dice roll. Sometimes a roll worth taking, but still a roll.
For me, Mac/Win/Linux/Android/iOS support is more than enough, and I think it will be enough to bootstrap the language into some level of industry acceptance. It depends on the project really, and how much the developer values console support over the benefits that a modern systems language like Rust provides.
# compute average line length
var count = 0
var sum = 0
for line in stdin.lines:
count += 1
sum += line.len
echo "Average line length: ",
if count > 0: sum / count else: 0
Type inference ensures that this uses efficient types internally, and is compiled to something very close to the efficiency of C. Here [2] is the generated C code, minus line tracing and stack trace frame generation. (Nimrod does things like bounds checking and overflow checking; without them, the program obviously becomes faster; the AddInt() function, for example, is replaced with a simple "+=".)If so, I believe Nimrod's support for immutability gets you pretty far, but I have not looked very deeply at it. For example, implements an explicit "IO taint" mechanism reminiscent of Haskell.
As for Go, it's not very well suited to this domain. Or at least no better suited than Java or C#.
Indeed. Most of the trail blazing work is being done by professional game devs in their spare time, hobbyists, indies and students who are willing to endure the pains of early adoption for the incredible gains that Rust gives them. It will only be post 1.0 however that the bigger players will be able to even consider using Rust. It's too risky to bet an entire company on – and that's coming from somebody who is willing to bet their indie project on it. ;)
The security track record of applications written in C++ disagrees with you.
Modern C++ is really safe if you use the subset that involves automatic storage duration, well bounded arrays, etc and use all the warning flags of your compiler, run static analysis, have a robust test framework, etc.
Consider iterator invalidation, null pointer dereference (which is undefined behavior, not a segfault -- and you can't get away from pointers because of "this" and move semantics), dangling references, destruction of the unique owner of the "this" pointer, use after move, etc. etc.
* Iterator invalidation: if you destroy the contents of a container that you're iterating over, undefined behavior. This has resulted in actual security bugs in Firefox.
std::vector v;
v.push_back(MyObject);
for (auto x : v) {
v.clear();
x->whatever(); // UB
}
* "this" pointer invalidation: if you call a method on an object that is a unique_ptr or shared_ptr holds the only reference to, there are ways for the object to cause the smart pointer holding onto it to let go of it, causing the "this" pointer to go dangling. The simplest way is to have the object be stored in a global variable and to have the method overwrite the contents of that global. std::enable_shared_from_this can fix it, but only if you use it everywhere and use shared_ptr for all your objects that you plan to call methods on. (Nobody does this in practice because the overhead, both syntactic and at runtime, is far too high, and it doesn't help for the STL classes, which don't do this.) class Foo;
unique_ptr<Foo> inst;
class Foo {
public:
virtual void f();
void kaboom() {
inst = NULL;
f(); // UB if this == inst
}
};
* Dangling references: similar to the above, but with arbitrary references. (To see this, refactor the code above into a static method with an explicit reference parameter: observe that the problem remains.) No references in C++ are actually safe.* Use after move: obvious. Undefined behavior.
* Null pointer dereference: contrary to popular belief, null pointer dereference is undefined behavior, not a segfault. This means that the compiler is free to, for example, make you fall off the end of the function if you dereference a null pointer. In practice compilers don't do this, because people dereference null pointers all the time, but they do assume that pointers that have been successfully dereferenced once cannot be null and remove those null checks. The latter optimization has caused at least one vulnerability in the Linux kernel.
Why does use after free matter? See the page here: https://www.owasp.org/index.php/Using_freed_memory
In particular, note this: "If the newly allocated data chances to hold a class, in C++ for example, various function pointers may be scattered within the heap data. If one of these function pointers is overwritten with an address to valid shellcode, execution of arbitrary code can be achieved." This happens a lot—not all use-after-free is exploitable, of course, but it happened often enough that all browsers had to start hacking in special allocators to try to reduce the possibility of exploitation of use-after-frees (search for "frame poisoning").
Obligatory disclaimer: these are small code samples. Of course nobody would write exactly these code examples in practice. But we do see these issues in practice a lot when the programs get big and the call chains get deep and suddenly you discover that it's possible to call function foo() in one module from function bar() in another module and foo() stomps all over the container that bar() was iterating over. At this point claiming that C++ is memory safe is the extraordinary claim; C++ is neither memory safe in theory (as these examples show) nor in practice (as the litany of memory safety problems in C++ apps shows).
Examples of things efficiently implemented entirely in the standard library in pure Rust (well, with some calls into the operating system/libc): Vec, the std::vector equivalent. Rc, reference counted pointers (statically restricted to a single thread). Arc, thread-safe reference counted pointers. Mutex. Concurrent queues. Hashmap.
I think it is a valid criticism of the language that not all non-primitive types aren't implicitly const, though. But you could never implement that without colossal backwards compatibility breakage. Which I guess is fine, since you could just keep a code base an std= behind until you fixed it.
> Use after move: obvious. Undefined behavior.
This I don't have an answer to though. I've always disliked how this isn't a compiler error.
Besides, isn't "C++ is memory safe if you don't use mutation" (even if it were true—which it isn't) an extremely uninteresting statement? That's a very crippled subset of the language.
Mutability in Rust is perfectly safe because of the static checks built into the type system – the compiler will catch you if you screw things up.
> you could never implement that without colossal backwards compatibility breakage
I cannot express how important immutability as default is. This prevents the issues that C++ has with folks forgetting to mark things as const. There is also lint that warns when locals are unnecessarily marked as mutable, which can catch some logic errors (I say that from experience).
Also note that I said 'immutability' not 'const'. Immutability is a far stronger invariant than const, and therefore is much safer. It could also lead to better compile-time optimisations in the future. I'm sure you know this, but just in case:
- const: you can't mutate it, but others possibly can - immutable: nobody can mutate it
I've posted a list of the things I consider the most relevant to game development: https://news.ycombinator.com/item?id=7587413 Any one or two of them alone wouldn't really be a compelling enough reason to switch, but put together they form a very compelling value proposition.
The fact that applications like browsers and operating systems (which are known to be high value targets) have a lot of effort & resources put into security but still have attack vectors makes the "C++ is secure" position fairly indefensible.
[1] http://en.wikipedia.org/wiki/Battlefield_4#Technical_issues_...
If on the other hand the devs understand which classes of bugs aren't ruled out in Rust, then sure, you will end up with fewer bugs.
That is really important, but still, wouldn't it be better if you could encode at least some of those good practices into the language itself, rather than relying on humans to be constantly on their game? I'm certainly not perfect, so I would rather my sloppiness be caught earlier rather than having it come back to bite me in the future. See: http://thecodelesscode.com/case/116
- No null pointers (with an Option type that compiles down to a nullable pointer)
- Data race free concurrency
- Zero cost abstractions
- RAII and destructors
- No exceptions
- A modern, highly expressive type system
- Generics that throw type errors at the call site, not deep in a template expansion
- True immutability (not `const`)
- An excellent C FFI
- Compiles to native code
- You don't pay for what you don't use
- Safe by default, but you can circumvent the safety checks if you know what you are doing, and those places are clearly marked and easy to audit. Inline assembly is supported.
- Most safety is enforced statically, so you don't pay for it at run time.
No exceptions is my only objection, but I know they're dubious for a systems language.
Regarding exceptions: whilst they can be be very useful, unfortunately a significant number of large, performance sensitive C++ projects outlaw them due to overhead and safety concerns (the semantics can become quite hairy when mixed with destructors). The Rust developers felt that it was easier to forgo them entirely.
My understanding is that the old exception code called "SJLJ" (short for setjmp, longjmp, which is what it was) was slow. I think each try/catch required hooks, and yes, it was.
The newer compilers generate something called "DWARF"; resources on it are unfortunately scarce, but my understanding is that you don't pay anything in speed for an exception until you throw one. (You do however pay a bit of disk/memory for data about where try/catch handlers are, I think.)
> safety concerns (the semantics can become quite hairy when mixed with destructors)
I'm assuming that you shouldn't throw in a destructor.¹
This argument, to me, always needs more information attached to it, because by itself it's meaningless. Assuming the alternative is returning either the result, or an error code, you run into exactly the same semantic issues, you're just handling them manually now. Is that better, and how?
In the manual case. If I assume I have some code that returns to me an error code that I can't handle, I need to propagate that error up to a stack frame that can. Thus, I begin to manually unwind the stack, during which, I destruct things. If we're assuming destructors can throw², then I can potentially run into the problem of having two errors: now what do I do?
C++ isn't the only language here: C#, Python, and Java share the problem of "What do you do in the face of multiple exceptions requiring propagation up the stack?", though I think C++ is the only one that solves it by terminating the program. I believe C# and Python just drop the original exception, and I have no idea what Java does. Honestly, if things are that effed up, terminate doesn't sound that bad to me. In practice in C++, most destructors can't/don't throw. (Files are about the hairiest thing, since flushing a file to disk on close can fail: C++'s file classes will ignore failures there, which doesn't exactly sit well with me. You can always flush it manually before closing, but of course, if you do this during exception propagation and throw on failure, you risk termination due to two exceptions.)
Even C has this, in that if you're propagating an integer error code up the stack, and something goes wrong in a cleanup, you've got this problem. In C, you're forced to choose, of course, including the choice of "ignore the problem entirely".
That said, I'll add the answer for Rust here. (I've never used Rust, so correct me if I'm wrong. I'm going abstract away the Rust-specific types, however.) Rust, for a function returning T but that might fail, returns either Optional<T> or a Result<T>-ish object, which is basically (T or ErrorObject). Rust has strong typing, so if there's an error, you can't ignore it directly, because you can't get at the result. And if you try, it terminates the "task". Strong typing is the winner here. (This reminds me why I need to look into Rust.)
¹It's not illegal to do so, but since destructors get called while an exception unwinds the stack, you can potentially run into an exception causing an exception. Two exceptions in C++ result in a termination of the program.
²If we're not, then exceptions are perfectly safe.