The primary client language of TensorFlow is Python, but there are projects to support other programming languages. All clients are based on the same backend, which is accessible through the TensorFlow C API. It provides, for example, the following function for creating a tensor:
The demo is built on top of this C API, showing how a TensorFlow client in Kotlin/Native could look like.
Kotlin/Native allows to compile Kotlin code into native binaries, with potential targets such as embedded applications, iOS and WebAssembly. It is currently in pre-release, but turned out to be very usable already for my use case. Similar to how Kotlin is interoperable with Java libraries, Kotlin/Native is compatible with existing C libraries. Compared to C, Kotlin provides many modern language features, such as generics and extension functions, and brings some convenience of the Kotlin standard library into native code, for example for sequence manipulation (filter, map, …).
Fortunately, TensorFlow has good documentation on getting started with its C library and even has some instructions on how to build a TensorFlow client in new language. Additionally, I found it helpful to have a look at the test cases to understand the C API. Also, Kotlin/Native’s C interop features are well documented.
The build script performs of the three following steps:
1: Install the TensorFlow binary for CPU or GPU:
2: Call the Kotlin/Native tool to generate bindings for the TensorFlow library:
tensorflow.def specifies the headers that we want to include in the bindings, in our case the complete TensorFlow C API:
This step also generates corresponding Kotlin wrappers for the C API, for example:
3: Calling the Kotlin/Native compiler and linker to obtain an executable:
Alternatively to the shell script, an equivalent gradle script is available, streamlining some of this process. Now we are left with fun part:
Writing the client code
To increase readability, we can use Kotlin’s type alias feature:
Most TensorFlow functions take a status argument that allows to check whether there was an error. We can streamline checking for errors by defining the following extension functions:
The following function takes a block of code and makes sure that the status is ok after it was executed:
This simplifies defining operations in our graph class, for example the ones for defining a constant and a graph input:
Having defined constants and inputs, we still cannot calculate anything. To add two tensors, we define addition:
To allow using the plus sign, we also overloaded the operator. A mechanism ensuring uniqiue operator names would have to be implemented for multi-use, but as we only use the plus operation once in this demo, we don’t need it.
The following function allows creating scalar (0-dimensional) tensors:
In TensorFlow, actual execution of graphs on data is done in sessions. To run a session, we need a function that takes input values assigned to each input and returns output tensors values for the queried outputs:
The most important bit of the
invoke function is the
Implementing the session in full adds some ceremony, mainly due to input/output memory allocation and dispose operations.
I added the following function to the graph class to streamline running sessions:
To put everything together, we define a small graph that adds 2 to any given input. We execute the graph on a session and feed 3 as an input:
That’s it! We have seen how the TensorFlow backend can be used from Kotlin/Native. The full code is available in the Kotlin/Native repository along with instructions for how to run it. If you have questions or feedback, please comment below.
Some thoughts on the viability of using Kotlin/Native for machine learning are outlined at the end of the follow-up post, which describes how a handwritten digit classifier can be trained in Kotlin/Native using Torch.