Inko 0.16.0 released

Published on

We're pleased to announce the release of Inko 0.16.0. This release includes support for TLS sockets, automatically installing dependencies, and more.

Table of contents

For the full list of changes, refer to the changelog.

A special thanks to the following people for contributing changes included in this release:

We'd like to thank the NLnet foundation for sponsoring part of the work that went into this release.

Support for TLS sockets

Thanks to the NLnet foundation for sponsoring the development of this feature.

Inko 0.16.0 introduces support for TLS 1.2 and TLS 1.3 sockets using the new std.net.tls module, built on top of rustls instead of using OpenSSL. Compared to OpenSSL, rustls offers various benefits:

  • It has a better history when it comes to security and code quality
  • It makes cross-compilation of Inko's runtime easier, as one doesn't need to also compile OpenSSL for each platform.
  • Using rustls and rustls-platform-verifier, Inko is able to verify certificates using a platform's native verification mechanism, something OpenSSL doesn't support.
  • The rustls API is more pleasant/easier to use and makes it much more difficult to shoot yourself in the foot by accident.

Our initial plan was to write the TLS stack from scratch in Inko itself. We decided against this due to the sheer amount of work required to make this happen, along with the various language additions needed to support this ( AES hardware intrinsics, constant-time cryptographic primitives, etc). Over time we'll likely move towards a pure Inko TLS stack, but this isn't a priority for the foreseeable future.

The std.net.tls module offers types for both TLS clients and servers. For example, using std.net.tls.Client we can perform a simple HTTPS 1.0 request as follows:

import std.net.ip (IpAddress)
import std.net.socket (TcpClient)
import std.net.tls (Client, ClientConfig)
import std.stdio (STDOUT)

class async Main {
  fn async main {
    let sock = TcpClient.new(ip: IpAddress.v4(1, 1, 1, 1), port: 443).or_panic(
      'failed to connect to the server',
    )

    # The configuration details (e.g. the certificates) to use for a client. You
    # should create these once and then clone the value whenever necessary.
    # Using `ClientConfig.new` means Inko will use the host platform's mechanism
    # for validating certificates. You can also use a custom certificate using
    # `ClientConfig.with_certificate`.
    let conf = ClientConfig.new

    # The return type of `Client.new` is `Option[Client]`, and will be a `Some`
    # if the server name is valid, otherwise a `None` is returned.
    let client = Client.new(sock, conf, name: 'one.one.one.one').or_panic(
      'the server name is invalid',
    )

    client
      .write_string('GET / HTTP/1.0\r\nHost: one.one.one.one\r\n\r\n')
      .or_panic('failed to write the request')

    let resp = ByteArray.new
    let stdout = STDOUT.new

    client.read_all(resp).or_panic('failed to read the response')
    stdout.print(resp.into_string)
  }
}

Using std.net.tls.Server we can create a TLS server:

import std.crypto.x509 (Certificate, PrivateKey)
import std.net.ip (IpAddress)
import std.net.socket (TcpServer)
import std.net.tls (Server, ServerConfig)
import std.stdio (STDOUT)

class async Main {
  fn async main {
    # Normally you'd read the certificate and private key from a file or a
    # database, but for the sake of simplicity we're using (invalid) dummy data
    # here.
    let cert = Certificate.new(ByteArray.from_array([1, 2, 3]))
    let key = PrivateKey.new(ByteArray.from_array([4, 5, 6]))

    # This creates the configuration to use for server connections. This returns
    # a `Result` as the operation may fail, such as when the private key or
    # certificate contains invalid data.
    let conf = ServerConfig.new(cert, key).or_panic(
      'failed to create the server configuration',
    )

    # This sets up the TCP socket to use for accepting new connections.
    let server = TcpServer
      .new(ip: IpAddress.v4(0, 0, 0, 0), port: 9000)
      .or_panic('failed to start the server')

    # Now we can accept a new connection, wrapping it in a `Server` instance.
    let con = server.accept.map(fn (sock) { Server.new(sock, conf) }).or_panic(
      'failed to accept the new connection',
    )

    let bytes = ByteArray.new
    let stdout = STDOUT.new

    con.read(into: bytes, size: 32).or_panic('failed to read the data')
    stdout.print(bytes.into_string)
  }
}

For more details, refer to issue #329 and commit a9c7bd7.

Support for encoding and decoding base64 data

The standard library provides the new module std.base64, which in turn provides types for encoding and decoding data as/from base64.

Encoding is done using std.base64.Encoder:

import std.base64 (Encoder)
import std.stdio (STDOUT)

class async Main {
  fn async main {
    let base64 = ByteArray.new
    let stdout = STDOUT.new

    Encoder.new.encode('hello world'.to_byte_array, into: base64)
    stdout.print(base64.to_string) # => 'aGVsbG8gd29ybGQ='
  }
}

Decoding is done using std.base64.Decoder:

import std.base64 (Decoder)
import std.stdio (STDOUT)

class async Main {
  fn async main {
    let plain = ByteArray.new
    let stdout = STDOUT.new

    Decoder.new.decode('aGVsbG8gd29ybGQ='.to_byte_array, into: plain)
    stdout.print(plain.to_string) # => 'hello world'
  }
}

For simple cases you can also use the methods std.base64.encode and std.base64.decode.

For more details, refer to commit 09ad36e.

Automatic installation of build dependencies

Thanks to Tomoki Aonuma, Inko now automatically installs dependencies when running commands such as inko build, inko test and inko doc, removing the need for manually running inko pkg sync:

$ ls dep
ls: cannot access 'dep': No such file or directory

$ inko build
Downloading https://github.com/yorickpeterse/inko-wobsite v0.18.0
Downloading https://github.com/yorickpeterse/inko-builder v0.13.0
Downloading https://github.com/yorickpeterse/inko-markdown v0.20.1
Downloading https://github.com/yorickpeterse/inko-syntax v0.9.0

$ ls dep
5a0dc78739a032c3b9ba351cdc84abbeb5472ef757699f6e7acb077b9e412071  6eca2ee9336610b3b5fafa7675508b7c76aae0d7748d089ad2e6ec68d017947b  hash
5e9313d54ba89b1b633b439b5900aadcbecc41bd97aabd10bee95ac8a07a8ae5  a11b9ff96cc9ab66a68aa775c66b7e0436a6ed4416afa47fdf1559de232e3b51

You can of course still run inko pkg sync manually in case you want to install the dependencies ahead of time.

For more details, refer to pull request #742 and commit 3047976.

Requiring a minimum Inko version to build a project

Also thanks to Tomoki Aonuma, you can now specify a minimum version of Inko that's required to build a project by running inko pkg add inko X.Y.Z where X.Y.Z is the version. For example, to require 0.16.0 or newer you'd run the following in your project:

inko pkg add inko 0.16.0

The required version is stored in the package manifest (inko.pkg), so make sure to track this file in Git if not done so already.

For more details, refer to pull request #739 and commit bc06c3e.

Less data is cloned when downloading dependencies

Inko dependencies are Git repositories hosted on platform such as GitHub and GitLab. These packages are downloaded using git clone, after which the necessary files are copied into a project's dep/ directory.

Thanks to Tomoki Aonuma, we now clone only the data we need to install a package into a project, instead of cloning the entire repository. This should speed up the download process and require less disk space when cloning large repositories.

For more details, refer to pull request #737 and commit ab90f67.

Generics now support C types of different sizes

Inko used to not allow C types such as Int16 and Float32 to be passed to generic type parameters, thus disallowing types such as Array[Float32]. Starting with 0.16.0 this is supported and allowed, and the Array type is changed to use the correct size for its memory buffer based on the data it stores, instead of using a fixed size of 8 bytes.

In isolation this isn't terribly interesting, in particular since we still disallow C structures in generic contexts, but it paves the way for stack allocating types of different sizes (instead of everything being heap allocated) and being able to use them in generic contexts, something we aim to implement in the future.

For more details, refer to commit f156a36.

LLVM 17 or newer is required

Starting with 0.16.0, LLVM 17 or newer is required to build the compiler. Newer versions might also work depending on the changes introduced by LLVM, but we recommend using LLVM 17 whenever possible.

For more details, refer to commit f3d5cd5.

Rust 1.78 or newer is required

The version requirement of Rust has been increased from 1.70.0 to 1.78.0. Building Inko using an older version of Rust might work but isn't supported.

For more details, refer to commit 63086b2.

Building the compiler is easier on macOS and FreeBSD

Starting with 0.16.0 it's easier to build Inko's compiler on macOS and FreeBSD, as you no longer need to set the LIBRARY_PATH and PATH variables to ensure Rust is able to find the necessary LLVM libraries and executables.

For more details, refer to this llvm-sys merge request and commit f5aa237.

Removal of deprecated syntax

In the release post for 0.15.0 we announced the deprecation of the class literal and trailing closure syntax, and support for this syntax is removed in 0.16.0. If you're still using 0.14.0 or older, first upgrade to 0.15.0 and run inko fmt to update the syntax, then upgrade to 0.16.0.

For more details, refer to commit 79c0220.

Performance improvements

The following performance improvements are included in this release:

Bug fixes

The following bug fixes are included in this release:

Following and supporting Inko

If Inko sounds like an interesting language, consider joining the Discord server. You can also follow along on the /r/inko subreddit.

If you'd like to support the continued development of Inko, please consider donating using GitHub Sponsors as this allows us to continue working on Inko full-time.