Last year, I attempted to create my own desktop (that is macOS) Flutter app because for a talk I gave, I wanted to present my Flutter-based slide deck as if it was Keynote. Unfortunately, I never published that application, but I documented my process in dabbling around with macOS desktop programming and here are those notes (translated to english).

Why? Why not!

I knew that there was an officially unofficial Flutter desktop project already but I wanted to learn how to do it myself, just following the instructions on the Flutter Github project. In the meantime, other people did the same as I did and created variants for Go and Rust which are much more complete. Use those projects.

GLFW

I’m familiar with iOS (and Android) but I never created a native macOS app. So I had to familiarize myself with the recommended GLFW library first. This C library (hopefully) abstracts away all that low-level OpenGL and macOS stuff.

So perhaps this article is be also useful for Linux or Windows users.

Setup

Using Homebrew, I install the current version (3.2.1) of GLFW:

$ brew install glfw3

Using Xcode (version 10.1 at the time of this writing), I create a “Command Line Tool” called “gltest” for the “macOS” platform.

In the target’s configuration, under “Build Settings”, I add /usr/local/include as a “Header Search Path” and /usr/local/lib as a “Library Search Path”. Then, I add -lglfw to “Other Linker Flags”.

Having to modify the Xcode build settings always feels a bit dangerous because all that options are a bit magical… but I know how to use a C compiler from times too distant to remember and I feel, I know what I’m doing here.

The next magical thing is to create a so called bridging header. I haven’t mentioned that I don’t want to use C (or C++) but Swift, just because I can, and because I’m much more familiar with that language than with C++.

(In hindsight, this was a mistake because it took me at least twice as long to figure out how to access the low-level C API than to swallow my pride and use my rusty C knowledge. At least, now I’m somewhat familiar with those UnsafePointer thingys in Swift.)

Here is the contents of a file I call includes.h :

#include <GLFW/glfw3.h>

I set it in the target’s configuration as the project’s “Objective-C Bridging Header”.

Writing Code

In main.swift (pre-generated by Xcode), I add this code, on the fly translated from C to Swift from some GLFW example which I found on the project’s home page, if I remember correctly:

guard glfwInit() != 0 else {

print("failed to initialize")

exit(1)

} defer {

glfwTerminate()

} guard let window = glfwCreateWindow(800, 600, "Test", nil, nil) else {

print("failed to create window")

exit(1)

} glfwMakeContextCurrent(window) glClearColor(1, 0, 0, 1) while glfwWindowShouldClose(window) == 0 {

glClear(UInt32(GL_COLOR_BUFFER_BIT))



// Draw a square using the (deprecated) fixed pipeline functionality

glColor3f(1, 1, 0)

glBegin(UInt32(GL_QUADS))

glVertex2f(-0.5, -0.5)

glVertex2f(0.5, -0.5)

glVertex2f(0.5, 0.5)

glVertex2f(-0.5, 0.5)

glEnd()



glfwSwapBuffers(window)



glfwPollEvents()

} glfwDestroyWindow(window)

This code …

Initializes the GLWF library and aborts if this fails.

Makes sure that everything is de-initialized correctly at the end.

Shows a 800x600 pixel application window with a title called “Test”.

Binds (somehow) OpenGL to that window.

Fills the window with a red (1, 0, 0) background color.

Loops until the user clicks the window’s close button.

Draws a yellow “square” (I even copied the deprecation waring comment…).

Displays the drawing by using some kind of double buffering.

Pumps events so that the close event is detected, I guess.

Hides (and destroys) the window.

Unfortunately, it doesn’t work.

I had to learn that since 2017, GLFW 3.2.1 doesn’t work anymore on a Mac.

Bummer!

Rewind and Restart

You need to use version 3.3.x, they say. However, this version isn’t available with Homebrew by default, so I had to switch to the HEAD version and compile it myself:

$ brew uninstall glfw

$ brew install — HEAD glfw

$ brew uninstall cmake

Now my example works … sort of.

There’s a big warning by Xcode that starting with macOS 10.14 (which is the version I’m currently using), OpenGL is deprecated. I silence the tool by switching the “Deployment Target” to 10.13.

Still, I wonder whether Flutter, which requires OpenGL as far as I know, will still work on macOS 10.15 which probably will be released in Q3 2019. Hopefully, because Flutter uses the same graphics library as Chrome, somebody is already working on porting Skia to work with Apple’s Metal API.

A yellow “square”

But my first goal is achieved: I can create a macOS OpenGL app with Swift (and without C or C++) using Xcode on my Mac.

Download the Flutter Framework

To get the required macOS framework, I have to download a magical URL like this (wrapped only for readability):

The value for FLUTTER_ENGINE can be retrieved “easily” like so:

$ cat `dirname \`which flutter\``/internal/engine.version

403337ebb893380101d1fa9cc435ce9b6cfeb22c

The version of the library you download and the version of your local Flutter installation must match, I think. I expect that at the time you (yes, you!) are reading this, your Flutter installation has a different engine version.

Extract the Framework

The downloaded ZIP contains another ZIP (unfortunately with the same name, so I had to introduce a tmp folder) which I extract as FlutterEmbedder.framework into my “gltest” (which I shouldn’t reuse) project folder:

$ unzip FlutterEmbedder.framework.zip -d tmp

$ unzip tmp/FlutterEmbedder.framework.zip -d \

FlutterEmbedder.framework

$ rm tmp/FlutterEmbedder.framework.zip

$ rmdir tmp

$ rm FlutterEmbedder.framework.zip

I add this framework to the project’s Linked Frameworks and Libraries.

And I have to extend my bridging header includes.h like so:

#include <GLFW/glfw3.h>

#include <FlutterEmbedder/FlutterEmbedder.h>

Test It

I’m now able to compile and run main.swift with this content:

print(FLUTTER_ENGINE_VERSION)

So the second goal is achieved by reading the printed 1 .

First Demo App

Before I continue, I need to create a Flutter app called demo :

$ flutter create demo

This is the standard counter example we all know and love.

Note, if suddenly flutter create doesn’t work anymore, you might have made the same mistake as I and defined an environment variable called FLUTTER_ENGINE earlier. Then, the `flutter` command line tool thinks it shall behave differently. That took me quite some time to figure out.

Now run the Flutter app once or at least build the asset bundle like so:

$ cd demo

$ flutter build bundle

$ cd ..

The Desktop App

All I have to do is combining the GLFW example with the initialization code for the Flutter framework and connect this to my demo application. Should be easy, shouldn’t it?

I can reuse this initialization (where I renamed “Test” to “Demo”):

guard glfwInit() != 0 else {

print("failed to initialize")

exit(1)

} defer {

glfwTerminate()

} guard let window = glfwCreateWindow(800, 600, "Demo", nil, nil) else {

print("failed to create window")

exit(1)

} defer {

glfwDestroyWindow(window)

}

Instead of binding an OpenGL context and drawing a square, I will initialize Flutter by calling a runFlutter function, which I will show in a moment:

guard runFlutter(window) else {

print("cannot launch app")

exit(1)

}

That call is followed by some event loop to keep the app running until the user clicks the window’s close button. This, again, is code we know already:

while glfwWindowShouldClose(window) == 0 {

glfwWaitEvents()

}

So far, nothing is new and I understand what happens.

However, then I stepped into a world of pain, namely Swift’s foreign function static typing pain. When it came to deallocating allocated structures, I gave up. Writing the following code was an interesting experience — not.

private func runFlutter(_ window: OpaquePointer) -> Bool {

// configure the renderer to use OpenGL

var config = FlutterRendererConfig()

config.type = kOpenGL

config.open_gl.struct_size = MemoryLayout<FlutterOpenGLRendererConfig>.size // setup required callbacks

config.open_gl.make_current = { userData in

glfwMakeContextCurrent(OpaquePointer(userData!))

return true

}

config.open_gl.clear_current = { _ in

glfwMakeContextCurrent(nil)

return true

}

config.open_gl.present = { userData in

glfwSwapBuffers(OpaquePointer(userData!))

return true

}

config.open_gl.fbo_callback = { _ in

return 0

}



// helper to get the current directory without using Foundation

func cwd() -> String {

return String(cString: getcwd(nil, 0))

}



// helper to allocate a C string on the heap

func ss(_ s: String) -> UnsafePointer<Int8> {

return UnsafePointer(strdup(s)!)

}



// setup flutter project paths

var args = FlutterProjectArgs()

args.struct_size = MemoryLayout<FlutterProjectArgs>.size let path = "/Users/sma/Documents/gltest"

args.assets_path = ss("\(path)/demo/build/flutter_assets")

args.icu_data_path = ss("\(path)/FlutterEmbedder.framework/Resources/icudtl.dat")



// create engine based on configuration and arguments

var engine: FlutterEngine? = nil

let result = FlutterEngineRun(Int(FLUTTER_ENGINE_VERSION),

&config, &args, UnsafeMutableRawPointer(window), &engine)

if result != kSuccess || engine == nil {

return false

}



// store engine for use in callback (see below)

glfwSetWindowUserPointer(window, UnsafeMutableRawPointer(engine))



// setup initial window size

resize(window)



// everything is fine

return true

} private func resize(_ window: OpaquePointer) {

var event = FlutterWindowMetricsEvent()

event.struct_size = MemoryLayout<FlutterWindowMetricsEvent>.size

event.width = 800

event.height = 600

event.pixel_ratio = 1



let engine = FlutterEngine(glfwGetWindowUserPointer(window)!)

FlutterEngineSendWindowMetricsEvent(engine, &event)

}

Some comments:

I need to setup three things: a configuration, arguments, an engine.

I need to setup a FlutterRendererConfig structure.

structure. I use OpenGL (not using it would be the other option).

Flutter wants to know how large such a structure is.

It seems, I have to setup four callbacks.

I’ve no idea what the fourth callback is for.

I need to setup a FlutterProjectArgs structure.

structure. Again, Flutter wants to know the size of that structure.

I have to provide the absolute path to flutter_assets .

. I have to provide the absolute path to icudtl.dat .

. These must be C strings copied to the heap.

I configure and run the Flutter engine.

It needs its version, configuration, arguments and a pointer to the OpenGL window and will write itself to a variable passed by reference.

An GLFW window can store some user information which can then be accessing in GLFW event callbacks. So a pointer to the engine is stored there.

I tell Flutter about the window size.

This took me much longer to write as I will ever admit. However, after hardcoding the path to my demo project and to the icudtl.dat file, I finally see the counter example running in my GLFW window. Yeah!

Don’t be shy my little app

Scaling the Display

Unfortunately, the Flutter app uses only one quarter of the window.

This is because my Mac has a retina display and while GLFW seems to automatically scale the 800x600 window setup, Flutter does not and I need to provide pixel values, not point values.

According to GLFW’s documentation I can get them like so:

var pxWidth: Int32 = 0, pxHeight: Int32 = 0

glfwGetFramebufferSize(window, &pxWidth, &pxHeight)

However, then fonts are still tiny. I need to set the pixel_ratio , too.

Therefore, I will ask GLFW for both the point and the pixel values and compute a global scaleFactor , hoping that width and height have the same factor and no call returns a width or height of 0.

// ... create window var width: Int32 = 0, height: Int32 = 0

glfwGetWindowSize(window, &width, &height) var pxWidth: Int32 = 0, pxHeight: Int32 = 0

glfwGetFramebufferSize(window, &pxWidth, &pxHeight) scaleFactor = min(

Double(pxWidth) / Double(width),

Double(pxHeight) / Double(height)) // ... run flutter

Here is the new resize function:

private func resize(_ window: OpaquePointer) {

var pxWidth: Int32 = 0, pxHeight: Int32 = 0

glfwGetFramebufferSize(window, &pxWidth, &pxHeight)



var event = FlutterWindowMetricsEvent()

event.struct_size = MemoryLayout<FlutterWindowMetricsEvent>.size

event.width = Int(pxWidth)

event.height = Int(pxHeight)

event.pixel_ratio = scaleFactor



let engine = FlutterEngine(glfwGetWindowUserPointer(window)!)

FlutterEngineSendWindowMetricsEvent(engine, &event)

}

Looks good. We’re nearly done.

A proud counter, still static though

Processing Events

To support resizing the window and to support maximizing it, we need to tell Flutter if GLFW detects this. This is easy:

// ... run flutter glfwSetFramebufferSizeCallback(window) { window, _, _ in

resize(window)

} // ... pump events

To make the app interactive, I want to track mouse events.

// ... resize glfwSetMouseButtonCallback(window) { window, key, action, mods in

if key == GLFW_MOUSE_BUTTON_1 && action == GLFW_PRESS {

var x: Double = 0, y: Double = 0

glfwGetCursorPos(window, &x, &y)

mouse(window, kDown, x, y)

glfwSetCursorPosCallback(window) { (window, x, y) in

mouse(window, kMove, x, y)

}

}

if key == GLFW_MOUSE_BUTTON_1 && action == GLFW_RELEASE {

var x: Double = 0, y: Double = 0

glfwGetCursorPos(window, &x, &y)

mouse(window, kUp, x, y)

glfwSetCursorPosCallback(window, nil)

}

} // ... pump events

This sets up a GLFW callback for mouse events. I’m tracking only the left button (button 1) and only “down”, “move” and “up” events. “move” events are only tracked while the button is pressed. For the counter example and for my slide deck application, this is enough.

To tell Flutter about the event, I use this private function:

private func mouse(_ window: OpaquePointer?,

_ phase: FlutterPointerPhase,

_ x: Double, _ y: Double) {

var event = FlutterPointerEvent()

event.struct_size = MemoryLayout<FlutterPointerEvent>.size

event.phase = phase

event.x = x * scaleFactor

event.y = y * scaleFactor

event.timestamp = time(nil)

let engine = FlutterEngine(glfwGetWindowUserPointer(window)!)

FlutterEngineSendPointerEvent(engine, &event, 1)

}

Note that I have to scale the x and y position. The original C++ example uses a very complicated looking expression to access some kind of high precision timer which I took the freedom to replace with a normal timer. I’ve no idea what side effects this might have. Just don’t click more than once per second and everything should be fine.

Let’s stop worrying and celebrate. We have a Flutter desktop application!

I spend more time on creating this animated GIF than creating the particle effect

Final achievement unlocked.

Distribute the App

There’s one teeny-tiny (frankly, there are probably more) problem: I cannot distribute my fancy app as long as it contains two hardcoded paths. To bundle something with my app, I need to create a real Cocoa App instead of a Command Line Tool.

Once Again With Cocoa

With Xcode, I create a new “Cocoa App” called “FlutterCounter”. It should use Swift and needs neither storyboards nor any other of the provided options.

As before, I add the GLFW library to the “Build Settings”.

As before, I add a bridging header.

And as before, I build against 10.13 to silence the deprecation warning.

I add my GLFW example code to AppDelegate.swift like so:





class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet weak var window: NSWindow! func applicationDidFinishLaunching(_ aNotification: Notification) {

main()

} ...

}

The code, as shown before, goes into a function called main .

Running the app shows my window — but is also shows a default Cocoa application window, probably created by MainMenu.xib which is still part of the project.

The correct way to proceed would probably to somehow add an OpenGL context to that window and not create my own GLFW window, but then I’d have to learn how to create native Cocoa apps with OpenGL — something I didn’t want to do. So I simply remove the “Window” and the “MainMenu” objects from the XIB file. (I learned the hard way that removing the XIB file itself is not a good idea.)

Next, I add the Flutter framework as before.

After I added my Swift code, the app fails to run.

I’m sad. Then I feel stupid.

I need to also embed the framework, not just link against it. That is, on the target’s “General” settings, first remove the library under “Linked Frameworks and Libraries” and then, under “Embedd Binaries”, click on “+”, choose the “FlutterEmbedder.framework” and press “OK”.

Then I change the path to that icudtl.dat file like so:

args.icu_data_path = ss("\(cwd())/../Frameworks/FlutterEmbedder.framework/Resources/icudtl.dat")

To also embed the flutter app, I create just another counter example in my current Xcode project. I call it counter this time. I build the assets bundle as before. Then, I add counter/build/flutter_assets as a “folder reference” (without copying files) to my Xcode project. Those file now magically appear under “Copy Bundle Resources” in “Build Phases” of my target.

Last but not least, I change the line that points to the assets to this:

args.assets_path = ss("\(cwd())/flutter_assets")

And … we’re still not done.

Flutter throws an “Unknown platform” exception.

More sadness. But without feeling stupid. Instead, I dig into the Flutter source to see that’s going one here. Looks like the Dart runtime (correctly) reports my platform as macos and Flutter’s TargetPlatform only knows about iOS, Android and Fuchsia.

I can circumvent this by defining the environment variable FLUTTER_TEST and running my app in debug mode only. I must have done this earlier, too, but then I probably forgot about it. Setting this variable of course doesn’t work for an application I want to deploy. A second variant is to use the global debugDefaultTargetPlatformOverride variable to TargetPlatform.android (because I like Material Design).

Finally, the app launches and I can count up the count.

One minor detail: Flutter complains about not being able to run the “Observatory”. This definitely didn’t happen before. My guess: A Cocoa application is somehow more restricted in what it is allowed to do and it probably cannot run an HTTP server.

Yepp.

I checked “Network: Incoming Connections (Server)” in the “Capabilities” of the “App Sandbox” and Flutter is happy — as I am.

I have created real macOS app!

A blast from the past — I should have removed the debug banner

Can I debug it?

Unfortunately not.

If I launch Visual Studio Code and try to attach it to the running process, the tool insists on starting either an iOS or Android simulator. It doesn’t detect the running desktop app.

Still, I have created by own Flutter desktop application and so can you!

… or use one of the much more developed projects I mentioned at the beginning of this article. Those support keyboard input, for example. Or plugins to access the main menu (which I deleted). Or detect if the application shall be closed (mine can’t be).