Tuesday, September 6, 2022

Running Rust and Flutter on a Kindle Fire 7

 Due to my strong interest in both Rust and mobile apps, I've been learning to use the flutter_rust_bridge package to build mobile apps using Rust and Flutter. They have a pretty clever example in their user guide in which the Rust code generates a Mandelbrot set which is rendered in the Flutter app. I wanted to try it out on an Android device. For development purposes, I'm using a Kindle Fire 7.

Unfortunately, their instructions are a bit cryptic, and in some places not entirely correct for the Kindle Fire 7 or for building on Windows. Here is what I had to do to get it working:

  • I cloned their GitHub repo as instructed. If you use an IDE, I suggest opening it up in Android Studio.
  • In the CI Workflow, I examined the flutter_android_test section. The bullets below described what I used or had to adapt.
    • I had installed NDK some time ago. The instructions there are for a Unix platform. For Windows, Android provides a useful guide. I think this is what I did, but I honestly don't remember for sure.
    • I already had Rust and Flutter installed. 
    • I next installed the cargo-ndk program: cargo install cargo-ndk
    • Here is where it was necessary to diverge from their instructions. The Kindle Fire 7 uses an ARM Cortex A7 CPU. So to target that CPU, I typed: rustup target add armv7-linux-androideabi in the rust subdirectory. This downloads the necessary files for rustc to cross-compile to that specific Android architecture.
    • Note that, other than disk space, there is no particular drawback to including as many distinct Android targets as you'd like. Each one you compile will be included in the APK, and at runtime it will try to select the correct .so file for the architecture on which it is running. 
    • At this point, I was ready to compile: cargo ndk -t armv7-linux-androideabi -o ..\android\app\src\main\jniLibs build
  • Having completed the above setup, flutter run built the APK, uploaded it to my device, and executed it perfectly.
I realized what the problem was after finding this answer on StackOverflow. I added my own answer as well, then wrote this blog post to add a few more details for any other Android Rust enthusiasts out there!

Monday, August 29, 2022

Socket Programming in Dart and Flutter

Much of the utility of mobile device apps comes from their ability to connect to other devices over a computer network. All modern operating systems for all network-enabled computing devices provide an important abstraction for making these connections: the TCP Socket. Each language, in turn, provides its own mechanisms for creating and using sockets to communicate.

Future, Async, Await

One of the trickiest issues involving sockets is timing. That is, the app probably has other things it can be doing while waiting for a message over a socket. In Flutter, there are three interrelated language constructs we use to manage timing:

  • Future objects
  • The async keyword
  • The await keyword
Any function in Dart can be labeled async. Every function labeled async returns a Future object immediately. The function then executes asynchronously with respect to the calling code. 

There are several ways in which the caller can interact with the returned Future object. The .then() method sets up a callback function to execute once the async function has completed successfully. The .catchError() method sets up a callback function to execute in the event that the async function throws an exception. And the .whenComplete() method's callback executes whenever the async function completes, error or not. These three methods can be thought of as analogous to try, catch, and finally, respectively.

Within an async function itself, it is pretty common to call another async function. In that case, the most common way to interact with the Future returned by the called function is to use the await keyword to pause the caller until the callee's Future completes. 

What works really well with this arrangement is that we can handle blocking communication without any explicit multithreading. Any networking code that blocks goes into an async function. 

Listening for Incoming Connections

The following examples come from a peer-to-peer text messenger app I wrote. Here is a code segment for setting up a server socket:
const int ourPort = 8888;

Future<void> _setupServer() async {
  try {
    ServerSocket server =
      await ServerSocket.bind(InternetAddress.anyIPv4, ourPort);
    server.listen(_listenToSocket); // StreamSubscription<Socket>
  } on SocketException catch (e) {
    _sendController.text = e.message;
  }
}
As _setupServer() is an async method, it needs to return a Future. Since it does not return any concrete values, it returns a Future<void>. It uses await to pause until it has acquired a ServerSocket instance. It then instructs the ServerSocket object to use _listenToSocket() as its callback by using the .listen() method. That method makes _listenToSocket()a subscriber to the ServerSocket
void _listenToSocket(Socket socket) {
  socket.listen((data) {
    setState(() {
      _handleIncomingMessage(socket.remoteAddress.address, data);
    });
  });
}
Within _listenToSocket(), there is no need for asynchronous code. Every time the ServerSocket receives an incoming connection, it executes _listenToSocket() and passes in the Socket it creates to communicate with the incoming connection. The _listenToSocket() method, in turn, listens for an incoming message from that socket. Once it arrives, it places the message on the user interface for the user to read. 

Because async is implemented behind-the-scenes with concurrent multithreading, data race conditions are a risk. In this program, the data that is shared between the implicit threads is all part of the user interface, represented by the _MyHomePageState class, of which _listenToSocket() is a method. As _MyHomePageState inherits from the State class, it inherits the .setState() method for updating the user interface. Each call to .setState() places the code passed to it in the UI event queue. The code passed through .setState() is thus effectively protected from data races on the UI state.

Sending Outgoing Messages

The app maintains a "friend" list of devices with whom the user can communicate. It retains the IP address, a nickname, and a message history with each friend. This data is encapsulated in the Friend class:
import 'package:mutex/mutex.dart';
final m = Mutex();

class Friend {
  String _ipAddr;
  String _name;
  List<Message> _messages;

  Friend(this._ipAddr, this._name) {
    _messages = [];
  }

  Future<void> send(String message) async {
    Socket socket = await Socket.connect(_ipAddr, ourPort);
    socket.write(message);
    socket.close();
    await _add_message("Me", message);
  }

  Future<void> receive(String message) async {
    return _add_message(_name, message);
  }

  Future<void> _add_message(String name, String message) async {
    await m.protect(() async => _messages.add(Message(name, message)));
  }
}
When the user sends a message to the specified Friend, the .send() method pauses via await while awaiting the acknowledgement of the connection and the creation of a Socket. The message history is stored in the _messages instance variable. Because messages can be appended to that list both when sent and received, concurrent data races are possible. To avert this issue, we use the mutex package. The .protect() method treats its code parameter as an atomic critical section. By using await, we can block until the mutex is free to allow the critical section to enter.

Review of Key Ideas

To write a TCP socket app using Flutter, it is important to understand the following key ideas:
  • Concurrency via Future, async, and await.
  • Subscribing callback code using .listen().