1. Introduction
Scala is one of the languages that is built on top of the JVM. The Scala compiler compiles the Scala files into JVM bytecode. As a result, it allows Scala to use any Java libraries directly in the Scala codebase. This helped Scala to get more attention at the initial stage.
However, the JVM is notorious for slow startup and also has a bigger memory footprint in general. Consequently, the Scala community is actively developing Scala Native, a way to create native Scala applications without the need for the JVM.
In this tutorial, we’ll look at the benefits of Scala Native. Then, we’ll use it to build a simple native application.
2. What Is Scala Native?
Scala Native is an optimizing ahead-of-time (AOT) compiler and a lightweight runtime for Scala. The Scala Native compiler plugin converts the Scala code directly into the machine language, removing the need for the JVM to run the application.
3. Advantages of Scala Native
Some of the advantages of using Scala Native are:
- Faster start-up time and low memory footprint
- Can utilize almost all powers of Scala to write native applications
- Support for writing optimized programs using low-level primitives
- Interoperability with native code written in C
- Useful for building applications such as command-line applications, embedded apps, serverless lambdas, and so on
4. Scala Native Overview
4.1. Compilation Flow
The Scala Native compiler converts the Scala file to an intermediate format called Native Intermediate Representation (NIR). NIR is a more enriched format of a TASTy/class file with additional low-level information such as linking hints and inlining. This NIR file is then converted into an LLVM IR file, which is yet another intermediate format. In the last step, the LLVM Static compiler will convert the IR file into the machine-dependent assembly format.
4.2. Language Semantics
We can use most of the Scala features in a Scala Native application. However, multi-threading is not yet supported. Also, the Scala Native team had to rewrite the Java Standard Library implementations due to licensing problems with Oracle. Nevertheless, it is not expected to cause any mismatch with standard Java libraries as it follows the same reference specifications.
5. Creating a Simple Native App
5.1. Dependencies
At development time, Scala Native is dependent on several tools:
- JDK 8 or above
- SBT
- LLVM/Clang 6.0 or above
5.2. Installation
We can create a Scala Native application by adding the SBT plugin like any other Scala project:
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.4")
Next, let’s enable the above plugin in the build.sbt file:
enablePlugins(ScalaNativePlugin)
Additionally, we can add the necessary library dependencies that the application requires. It’s important to note that we can only use the dependencies that are built for Scala Native.
Let’s add a supported library:
libraryDependencies ++= Seq(
"com.lihaoyi" %%% "fansi" % "0.3.0"
)
The %%% symbol after the groupId notifies the SBT to download the Scala Native version of these libraries.
5.3. Packaging as a Native Application
Now, let’s add a simple Scala file to the project:
object Main {
def main(args: Array[String]): Unit =
println(fansi.Color.Green("Hello, world from Scala-Native!"))
}
We can compile the code using the SBT command compile. Then, we can use the nativeLink command to create a native executable of this simple project:
sbt nativeLink
This will create the executable file under the target/scala-2.13 directory. We can then run this file as an executable, which will then print the text to the console.
6. Native Interoperability
Scala Native provides interoperability with many standard C libraries, and it supports direct integration with native C files.
6.1. C Standard Library Interoperability
Scala Native already provides bindings for many standard C libraries. That means we can write Scala code that will directly invoke the C methods. For instance, let’s use the string.h library bindings in our Scala code:
val s1 = "Scala"
val s2 = "Native"
val scalaNative: String = Zone { implicit z =>
val scalaNativeC: CString = string.strcat(toCString(s1), toCString(s2))
println(fromCString(scalaNativeC)) // Prints ScalaNative
fromCString(scalaNativeC)
}
The method string.strcat() invokes the underlying C method to concatenate two strings. Zone does the necessary memory management for the allocation. Furthermore, once the block completes, Zone will free the allocated memory as well.
Scala Native also has support for C literal strings using the c interpolator:
val strLength: CSize = string.strlen(c"Hello ScalaNative")
println(s"Length of string is: "+strLength)
6.2. Custom C Code Interoperability
We can write custom C code and invoke it from Scala Native code with ease. First, we need to place the C file under the path src/main/resources/scala-native:
#include <string.h>
#include <stdio.h>
int check_length(char arr[]) {
int length = strlen(arr);
printf("Length of String `%s` : %d ", arr, length);
printf("\n");
return length;
}
Next, we need to create a binding in the Scala code using an object:
@extern
object sample {
def check_length(str: CString): Int = extern
}
The @extern annotation is used to mark the code that uses externally defined code. Then, we define a Scala method for the corresponding C code, using comparable types between C and Scala. The keyword extern in the method body signifies that the implementation is provided in some native code that is available on the linked libraries.
Now, we can access the method directly in Scala code as:
sample.check_length(c"Hello ScalaNative")
6.3. Third-Party C Library Interoperability
We can write complex low-level code in C that uses third-party libraries. This code can then be invoked from Scala Native in the same way as we would invoke normal methods. However, we need to install the necessary libraries in the target machine beforehand.
Let’s use the libcurl library to invoke a URL from C code:
#include <stdlib.h>
#include <curl/curl.h>
void function_pt(void *ptr, size_t size, size_t nmemb, void *stream) {
printf("%d", atoi(ptr));
}
int make_curl_call(char arr[]) {
CURL *curl;
CURLcode res;
curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, arr);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
res = curl_easy_perform(curl);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, function_pt);
if(res != CURLE_OK)
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
curl_easy_cleanup(curl);
}
return 0;
}
Now, we can define the extern method corresponding to the C method signature:
@extern
@link("curl")
object testcurl {
def make_curl_call(str: CString): Int = extern
}
The @link annotation lets the compiler know that we’re using the provided external library and that it needs to be linked during the nativeLink command. As in C, we omit the lib prefix from the library name.
Now, we can invoke the method by passing the relevant URL:
testcurl.make_curl_call(c"http://httpbin.org/uuid")
We assume that the libcurl library is installed in both the build machine and the target machine.
7. Conclusion
In this article, we looked at what Scala Native is and how we can use it to build native applications.
As always, the sample code used in this tutorial is available over on GitHub.