Discreet Log #12: Native Porting

21 Jul 2021

Welcome to Discreet Log! A fortnightly technical development blog to provide an in-depth look into the research, projects and tools that we work on at Open Privacy. For our twelfth post Dan Ballard goes over some work we've done to go the last mile on platform specific customization of our Cwtch Flutter app

We released Cwtch 1.1 last week, our first Beta update, with a ton of bug fixes and even a few new features and the two reasons we’ve been able to move this fast even with such a small team are both the maturity of Cwtch now as a platform, but also, because Flutter has been working well as a cross-platform framework with quality high level widgets that let us build the UI rapidly. That said, Flutter having good cross-platform widgets doesn’t mean it’s seamless frictionless work to bring it to 3 platforms, especially when support for the Desktop ones are still very new to Flutter.

We’ve already blogged about building Android service support into the Cwtch Flutter app as one big and ongoing example of that work. Additionally, each platform handles notifications differently, and so on Android our notification work overlaps with our service support work, as reported above. Today I wanted to take a look at some other platform specific work we’ve done for Windows and Linux.

Windows

Icons

One of the first things we noticed is that Flutter Engine doesn’t natively support app icons. This should have been obvious as we had to manually specify it in AndroidManifest.xml as well, but we’re used to special Android exceptions from a framework that otherwise more seamlessly handles desktop. In Flutter’s case though, no platform’s icons are handled. For Windows, the generated platform specific C code in windows has rudimentary support for setting a default icon. We could have just dropped our icon on top of the default one in windows/runner/resources and called it a day, but it turned out that scaled very poorly and often looked terrible. It seems like the Windows OS code for resizing icons hasn’t been updated since the 90s. Thankfully I also remember Windows programming from the late 90s and knew that it was usually typical to supply a pre-resized set of icons to compensate for this. Unfortunately the auto-generated code from Flutter in windows/runner/win32_window.cpp just populated the WNDCLASS with one icon. Additionally, after looking at the Windows API docs, WNDCLASS is deprecated and supposed to be replaced with WNDCLASSEX that also has support for setting a “small” icon to be used in the application window bar. This all led to the following modifications to our windows/runner/win32_window.cpp

const wchar_t* WindowClassRegistrar::GetWindowClass() {
  if (!class_registered_) {
    WNDCLASSEX window_class{};
    window_class.cbSize = sizeof(WNDCLASSEX); // required new setup for WNDCLASSEX per API docs
    ...
    int icon_sz = GetSystemMetrics(SM_CXICON);
    int iconh_id = IDI_APP_ICON_32;
    if (icon_sz > 128) {
        iconh_id = IDI_APP_ICON_256;
    } else if (icon_sz > 64) {
        iconh_id = IDI_APP_ICON_128;
    } else if (icon_sz > 48) {
        iconh_id = IDI_APP_ICON_64;
    } else if (icon_sz > 32) {
          iconh_id = IDI_APP_ICON_48;
    }
    window_class.hIcon = LoadIcon(window_class.hInstance, MAKEINTRESOURCE(iconh_id));

    // ... repeat picking size for and setting small icon ...

    RegisterClassEx(&window_class);

We also have to add all the new icons as resources to windows/runner/Runner.rc and windows/runner/resource.h.

With that done we’ve made Flutter on Windows behave in a much more Windows idiomatic way and gotten much better looking less scrunched icons on the Windows bar and window title bar.

Installer

Flutter does have a suggested path for packaging a Windows app with an MSIX installer however this produces a Universal Windows Platform (UWP) application, which has some limitations, including not being able to execute packaged binaries except through an exclusive API that seems to only have a C# implementation (blocking our launching of Tor). Also Microsoft has confirmed UWP is dying, losing support, and not on their roadmap any longer so it’s a double dead end for us.

To package our new Cwtch Flutter app we dusted off the NSIS installer code we had written for our old UI and updated it to work with the new one:

With this work repurposed and updated, we could now generate simple but pleasant and easy to use Windows installers for Cwtch.

Flutter Engine Issue with Caps Lock

Flutter Engine launched on Windows at 2.0 with a bug where the right shift key was sticky, and once pressed, could not be turned off. A user side code work around was proposed, and we implemented it while we wait for the engine fix to be published.

This involved adding lib/widgets/rightshiftfixer.dart and then using it by wrapping our main app widget in it in lib/main.dart

             home: appState.cwtchInit == true ? ShiftRightFixer(child: ProfileMgrView()) : SplashView(), 

We should be able to remove this awkward hack fix once the full fix is shipped in newer version of Flutter.

Windows Performance Problems

The Flutter Engine for Windows also appeared to have a big performance regression and only part of the fix was published, so we again manually had to update the generated code to use a new non efficiency broken run loop in windows/runner/run_loop.cpp

void RunLoop::Run() {
  // Fix/workaround for Windows high CPU usage
  // https://github.com/flutter/flutter/issues/78517#issuecomment-814843107
  MSG msg;
  while (GetMessage(&msg, nullptr, 0, 0)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
}

Linux

Icon

Flutter on Linux uses GTK to do basic window building and Window Manager interface. Sadly, at the time we had Flutter generate the linux platform code it didn’t appear to include any support for icons. GTK as a framework however does, so it was a simple one line add to linux/my_application.cc to have our Linux Flutter app have a proper icon.

   gtk_window_set_icon_from_file(window, "./cwtch.png", NULL); 

Linux Style Install

Flutter also has one supported way of packaging applications for Linux, and it is using Snaps. We’re slightly more fans of traditional packages (eg: .deb and .rpm) and Linux style system installs vs containers for user applications. It’s mostly a matter of preference.

The first problem before we could package our app like that however was that a Linux Flutter app cannot run with its libraries and assets spread out in different directories, it’s hard coded to load those resources from its executing directory. We had to read the Flutter engine code for Linux applications to see how it was loading libraries and assets and thankfully the Flutter engine C structs do provide ways of specifying the paths, even if in the Flutter generated code for your app it is then hard coded to the directory of the executable. We made the following changes so that the app could be run from the build directory, its own directory, from installed to $HOME or installed to the system under /usr in Linux/my_application.cc

  // Check if assets folder is relative to the executable or if we can use a system copy
  struct stat info;
  // if we're not in freshly compiled structure
  if (stat(fl_dart_project_get_assets_path(project), &info ) != 0 ) {
    if( stat("lib/cwtch", &info) == 0) {
      // use local dir structure
      project->assets_path = g_build_filename("data", "flutter_assets", nullptr);
      project->aot_library_path = g_build_filename("lib", "libapp.so", nullptr);
      project->icu_data_path = g_build_filename("data", "icudtl.dat", nullptr);
      gtk_window_set_icon_from_file(window,  "./cwtch.png", NULL);
    } else if( stat("/usr/share/cwtch/data/flutter_assets", &info ) != 0 ) {
      // if we're non in sys installed structure, use home dir structure
      struct passwd *pw = getpwuid(getuid());
      const char *homedir = pw->pw_dir;
      // /home/$USER/.local/share/cwtch/data/flutter_assets
      project->assets_path = g_build_filename(homedir, ".local", "share", "cwtch", "data", "flutter_assets", nullptr);
      // /home/$USER/.local/lib/cwtch/
      project->aot_library_path = g_build_filename(homedir, ".local", "lib", "cwtch", "libapp.so", nullptr);
      // /home/$USER/.local/share/cwtch/data
      project->icu_data_path = g_build_filename(homedir, ".local", "share", "cwtch", "data", "icudtl.dat", nullptr);
      gtk_window_set_icon_from_file(window,  g_build_filename(homedir, ".local", "share", "icons", "cwtch.png", nullptr), NULL);
    } else {
      // else assume we are in sys installed structure
      // /usr/share/cwtch/data/flutter_assets
      project->assets_path = g_build_filename("/", "usr", "share", "cwtch", "data", "flutter_assets", nullptr);
      // /usr/lib/cwtch
      project->aot_library_path = g_build_filename("/", "usr", "lib", "cwtch", "libapp.so", nullptr);
      // /usr/share/cwtch/data
      project->icu_data_path = g_build_filename("/", "usr", "share", "cwtch", "data", "icudtl.dat", nullptr);
      gtk_window_set_icon_from_file(window, "/usr/share/icons/cwtch.png", NULL);
    }
  }
  printf("my_application.cc: using aot_library_path or '%s'\n", project->aot_library_path);
  fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);

Since we were also shipping our own copy of libcwtch.so we needed to launch the application with a custom LD_LIBRARY_PATH so our install packaging scripts moved the Flutter binary into the lib folder and dropped in a simple one line shell script to invoke it properly. Also, as per observed practice of other applications, our bundled copy of tor went into the lib folder.

We wrote a script to assemble our actual release build directory with linux/package-release.sh, and you can see the rest of our invoke and install scripts in linux/.

Outstanding Platform Issues

Flutter on desktop is a new feature released this year and not everything works quite as expected. One issue we’re still waiting on Flutter to fix is that dragging scrolls bars is inverted, so dragging the scroll bar up scrolls down. We suspect this might be a mistake related to mobile gesture scroll dragging behaviour but regardless it’s a Flutter bug we hope they will address soon.

Additionally, we have received reports that in the current version of our app, Backspace stops working after a language change. This has purportedly been fixed upstream in Flutter, and we plan to upgrade the Flutter version on our build server for our next release to hopefully pass the fix on to our users.

Conclusion

Flutter provides some quality high level widgets that work cross-platform which has allowed us to rapidly build Cwtch, and improve it. We’re pleased with what we’ve seen so far, and excited with what it appears to be enabling us to deliver in the future. That said, it’s both a new tech, and there simply is no such thing as a perfect silver bullet for crossplatform app building. It still has some rough edges around some platform features which we’ve manually had to work around, and still have more work to do. Notification quality per platform still needs improving, as do other integrations like Windows system tray implementation.

If you’d like to see us keep going both in adding more features to Cwtch and on improving its platform specific integration, please donate!

Donate to Open Privacy



Stickers!

Donations of $5 or more receive stickers as a thank-you gift, and $25 or more gets you one of our new sticker sheets! To celebrate our 4th anniversary, we'll even count cumulative pledges since November 2021 to our Patreon.


Open Privacy is an incorporated non-profit society in British Columbia, Canada. Donations are not tax deductible. You can Donate Once via Bitcoin, Monero, Zcash, and Paypal, or you can Donate Monthly via Patreon or Paypal. Please contact us to arrange a donation by other methods.


What is Discreet Log?

Discreet Log is a technical development blog to give a more in-depth look at the research, projects and tools that we work on at Open Privacy.


More Discreet Logs