Rust 2018 came out ... but what is it?

Original author: Lin Clark, The Rust Team
  • Transfer
The article is written by Lin Clark in collaboration with the Rust development team (“we” in the text). You can also read the post on the official Rust blog.

The first version of Rust 2018 was released on December 6, 2018. In this release, we focused on performance so that the Rust developers can work as efficiently as possible. The timeline shows the transition of functions from the beta version to Rust 2018 and Rust 2015. It is surrounded by icons for tools and four areas: WebAssembly, embedded, networking and CLI. The red circle - the effectiveness of the developer - surrounds everything except Rust 2015 But in general it is not easy to explain what Rust 2018 is.






Some people present it with a new version of the language ... approximately it is, but not quite. I say “not really”, because here “new version” means not that new versions of other languages.

In most other languages, all new features add a new version. The previous version is not updated.

The Rust system works differently. This is due to how language develops. Almost all new features are 100% compatible with Rust. They do not require any changes. This means that there is no reason to limit them with the Rust 2018 code. New versions of the compiler will continue to support “Rust 2015 mode” by default.

But sometimes language development requires innovation, for example, a new syntax. And this new syntax can break existing code bases.

For example, the function async/await. Initially, there were no such concepts in Rust. But it turned out that these primitives are really useful, they simplify writing asynchronous code.

For this function, you must add keywords asyncandawait. But one should proceed with caution so as not to break the old code, where asyncor awaitcould be used as variable names.

Thus, we add keywords to Rust 2018. Although the function has not yet come out, keywords are now reserved. All incompatible changes for the next three years of development (for example, adding new keywords) are made at the same time in Rust 1.31. Although there are incompatible changes in Rust 2018, this does not mean that your code will break. Even if there are variables and the code will be compiled. By default, the compiler works as before. But if you want to use one of the new functions, you can choose the new compilation mode Rust 2018. The command



asyncawait

cargo fixwill tell you if you need to update the code to use new features and automate the process of making changes. Then you can add edition=2018to your Cargo.toml, if you agree on the use of new features.

This version specifier in Cargo.toml does not apply to the entire project and does not apply to your dependencies. It is limited to one particular crate. That is, you can simultaneously use Rust 2015 and Rust 2018 crates. Therefore, even when using Rust 2018, everything looks much the same as Rust 2015. Most of the changes are implemented simultaneously in Rust 2018 and Rust 2015. Only a few functions require incompatible changes. Rust 2018 is not only a change of the main language. Far not only they.









Rust 2018 is primarily a push to increase the productivity of Rust developers, thanks in large part to tools that are outside the language, as well as working out specific applications and understanding how to make Rust the most effective programming language for these cases.

Thus, you can represent Rust 2018 as a specifier in Cargo.toml, which is used to enable several functions that require incompatible changes ... Or you can imagine it at the point in time when Rust becomes one of the most effective languages ​​for many applications - when you need performance, efficient use of resources or high reliability.







We prefer the second definition. So let's look at all the enhancements made outside the language, and then dive into the language itself.

Rust for specific applications


A programming language cannot be effective by itself, in the abstract. It is effective in a particular application. Therefore, we understood that it was necessary not just to improve Rust as a language or a tool. It is also necessary to simplify the use of Rust in certain areas. In some cases, this meant creating a completely new set of tools for a completely new ecosystem. In other cases - polishing of already existing functions and good documentation in order to make it easier to lift and run a working system. The Rust development team has formed working groups in four areas:







  • WebAssembly
  • Embedded Applications
  • Network tasks
  • Command line tools

WebAssembly


WebAssembly had to create a completely new set of tools.

Only last year, WebAssembly made it possible to compile languages ​​like Rust to run on the Internet. Since then, Rust has quickly become the best language to integrate with existing web applications.



Rust works well for web development for two reasons:

  1. Cargo ecosystem crate works the way most web application developers are used to. Combine a bunch of small modules to form a larger application. This means that Rust is easy to use exactly where needed.
  2. Rust consumes few resources and does not require a runtime environment. Do not need a lot of code. If you have a tiny module that does a lot of hard computing work, embed a few lines of Rust to speed it up.

Using web-sys and js-sys crates from Rust code, it is easy to call web APIs such as fetchor appendChild. And wasm-bindgenit simplifies support for high-level data types that WebAssembly does not natively support.

After writing the Rust WebAssembly module, there are tools to easily connect it to the rest of the web application. You can use the wasm-pack to automatically run these tools and launch the module in npm if you wish.

For details, see the book “Rust and WebAssembly” .

What's next?


After the release of Rust 2018, developers plan to discuss with the community in which areas to work further.

Embedded Applications


For embedded development, it was necessary to increase the stability of the existing functionality.

In theory, Rust has always been a good language for embedded applications. This is a modern toolkit, which was sorely lacking for developers, and very convenient high-level language functions. All this without unnecessary load on the CPU and memory. So Rust is great for embedded.

But in practice it turned out differently. In a stable channelmissing required functions. In addition, for use on embedded devices, it was necessary to change the standard library. This means that people had to compile their own version of the Rust core crate (the crate used in each Rust application to provide the basic building blocks of Rust — the built-in functions and primitives). As a result, the developers depended on the experimental version of Rust. And in the absence of automatic tests, the experimental assembly often did not work on microcontrollers. To fix this, the developers tried to transfer all the necessary functions to a stable channel, add tests to the CI system for microcontrollers. This means that changing a desktop component will not break the embedded version.







With such changes, the development of embedded systems on Rust is moving from the field of advanced experiments to the area of ​​normal efficiency.

For details, see the book “Rust for embedded systems” .

What's next?


This year, Rust got really good support for the popular ARM Cortex-M family. However, many architectures are not yet well supported. You need to extend Rust for similar support for other architectures.

Network tasks


To use the network had to be built into the language of the key abstraction: async/await. Thus, developers can use standard idioms of Rust even in asynchronous code.

In network tasks often have to wait. For example, a response to a request. If the code is synchronous, the operation will be stopped: the core of the processor on which the code is executed cannot do anything until the request arrives. But in the asynchronous code, such a function can be put on standby, while the CPU core will do the rest for now.

Asynchronous programming is also possible in Rust 2015, and there are many advantages to this. In high-performance applications, the server application will handle many more connections per server. Embedded applications on tiny single-threaded CPUs optimize the use of a single thread.

But these advantages are accompanied by a major drawback: for such a code there is no validation of borrowing and you will have to use non-standard (and slightly confused) idioms of Rust. That is the benefit async/await. This gives the compiler the necessary information to check borrowing asynchronous function calls.

Keywords for are async/awaitimplemented in version 1.31, although they are not currently supported by implementation. Most of the work is done, and the function should be available in the next release.

What's next?


In addition to efficient low-level development, Rust can provide more efficient development of network applications at a higher level.

Many servers perform routine tasks: analyze URLs or work with HTTP. If you turn them into components — common abstractions that are shared as crates — then it will be easy to connect them to each other, forming all sorts of server and framework configurations.

An experimental Tide framework has been created for the development and testing of components .

Command line tools


For command-line tools, it was necessary to combine small low-level libraries into a higher-level abstraction and polish some existing tools.

For some scripts, bash is ideal. For example, to simply invoke other shell tools and transfer data between them.

But Rust is a great option for many other tools. For example, if you are creating a complex tool like ripgrep or a CLI tool on top of the functionality of an existing library.

Rust does not require a runtime environment and compiles into one static binary, which makes it easier to distribute the program. And you get high-level abstractions that are not found in other languages, such as C and C ++.

What else can improve Rust? Of course, abstractions are even higher.

With higher level abstractions, ready-made CLI is quickly and easily assembled.

An example of such an abstraction is the human panic library . In the absence of such a library, in the event of a failure, the CLI code is likely to produce the entire reverse trace. But it is not very interesting to users. You can add special error handling, but this is difficult.

With the human panic library, the output will automatically go to the error dump file. The user will see an informative message prompting you to report a problem and upload a dump file.



Getting started developing CLI tools is also easier. For example, the confy library automates its setting. He asks only two things:

  • What is the name of the application?
  • What configuration parameters do you want to provide (which you define as a structure that can be serialized and deserialized)?

All the rest will be determined by confy.

What's next?


We have abstracted many tasks for the CLI. But you can abstract something else. We are going to release more such high level libraries.

Rust Tools




When you write in a language, you work with its tools: starting with the editor and continuing with other tools at all stages of development and support.

This means that an effective language depends on effective tools.

Here are some new tools (and enhancements to existing ones) in Rust 2018.

IDE support


Of course, performance depends on quickly and smoothly transferring code from the developer’s mind to the computer screen. IDE support is crucial here. To do this, you need tools that can “explain” the IDE meaning of the Rust code: for example, suggest meaningful options for autocompleting lines.

In Rust 2018, the community focused on the functions required by the IDE. With the advent of the Rust Language Server and IntelliJ Rust, many IDEs now fully support Rust.

Faster compilation


Improving the efficiency of the compiler means accelerating it. This is what we did.

Previously, when you compiled the Rust crate, the compiler recompiled every single file in the crate. Now incremental compilation is implemented: it compiles only those parts that have changed. Along with other optimizations, this made the Rust compiler much faster.

rustfmt


Efficiency also requires that we never argue about the rules of code formatting and do not manually fix other people's styles.

The rustfmt tool helps with this: it automatically reformats the code in accordance with the default style (according to which the community has reached a consensus ). Rustfmt ensures that all Rust code matches the same style, similar to the clang format for C ++ or Prettier for JavaScript.

Clippy


Sometimes it's nice to have an experienced consultant nearby who gives advice on best practices when writing code. Clippy does this: it checks the code while viewing it and suggests standard idioms.

rustfix


But if you have an old code base with outdated idioms, then checking and correcting the code yourself can be tedious. You just want someone to fix the entire codebase.

In these cases, rustfix automates the process. It simultaneously applies the rules from tools like Clippy, and updates the old code in accordance with the idioms of Rust 2018.

Changes in Rust itself


Changes in the ecosystem have greatly increased the efficiency of programming. But some problems can be solved only by changes in the language itself.



As we said in the introduction, most language changes are fully compatible with existing Rust code. All these changes are part of Rust 2018. But since they break nothing, they work in any Rust code ... even in the old one.

Let's look at the important features that have been added to all versions. Then look at a small list of features of Rust 2018.

New features for all versions


Here is a small example of new features that are (or will be) in all versions of the language.

More accurate borrowing check


One big advantage of Rust is borrowing verification. It ensures that the code is safe for memory. But this is also a rather difficult feature for newcomers to Rust.

Part of the difficulty lies in learning new concepts. But there is another part ... Borrowing verification sometimes rejects code that seems to be working from the point of view of a programmer who fully understands the concept of memory security. A variable cannot be borrowed because it has already been borrowed. This happens because the borrowing lifetime supposedly had to extend to the end of its field — for example, to the end of the function in which the variable is located.






This meant that even if the variable completed its work with a value and no longer tries to gain access, other variables are still denied access to this value until the end of the function.

To correct the situation, we made the check smarter. Now she sees when the variable has actually completed using the value. After that, it does not block the use of data. While it is available only in Rust 2018, but in the near future the function will be added to all other versions. Soon we will write more on this topic.





Procedural macros in stable Rust


Macros in Rust were before Rust 1.0. But in Rust 2018, major improvements were made, for example, procedural macros appeared. They allow you to add your own syntax to Rust.

Rust 2018 offers two types of procedural macros:

Macros like functions


Macros like functions allow you to create objects that look like normal function calls, but are actually executed at compile time. They take one code and give another, which the compiler then inserts into a binary file.

They existed before, but with limited. The macro could only execute the match statement. He did not have access to view all the tokens in the incoming code.

But with procedural macros, you get the same input as the parser: the same stream of tokens. This means that you can create much more powerful macros like functions.

Attribute-like macros


If you are familiar with decorators in languages ​​like JavaScript, attribute macros are very similar. They allow you to annotate code snippets on Rust, which should be pre-processed and turned into something else.

The macro derivedoes exactly that. When you put it over a structure, the compiler takes this structure (after it has been analyzed as a list of tokens) and processes it. In particular, it adds the basic implementation of functions from the trait.

More ergonomic borrowing in comparisons


There is a simple change.

Previously, if you wanted to borrow something and tried to perform a comparison, you had to add some strange syntax: Now, instead of writing simply .



&Some(ref s)Some(s)

New features of Rust 2018


The smallest part of Rust 2018 is a function specific to this version. Here is a small set of changes in Rust 2018.

Keywords


Several keywords have been added to Rust 2018:

  • try
  • async/await

These functions are not yet fully implemented, but keywords have been added to Rust 1.31. Thus, in the future, we will not have to introduce new keywords (which would be an incompatible change) when we implement these functions.

Modular system


One big pain for newbies. Rust is a modular system. And I understand why. It was hard to understand why Rust chooses a particular module. To fix this, we made some changes to the path mechanism.

For example, if you imported a crate, you can use it on the way at the top level. But if you move any code to a submodule, it will no longer work.

// top level moduleexterncrate serde;
// this works fine at the top levelimpl serde::Serialize for MyType { ... }
mod foo {
  // but it does *not* work in a sub-moduleimpl serde::Serialize for OtherType { ... }
}

Another example is the prefix ::, which is used for both the root of a crate and an external crate. It is difficult to understand what is before us.

We made it more explicit. Now if you want to access the root crate, then use the prefix crate::. This is just one of the improvements for clarity.

If you want the current code to use the capabilities of Rust 2018, you will most likely need to update the code, taking into account new paths. But it is not necessary to do it manually. Before adding a version specifier to Cargo.toml, simply run cargo fix- and rustfixmake the necessary changes.

Additional Information


All information about the new version of the language contains the “Rust Guide 2018” .

Also popular now: