Skip to content

Latest commit

 

History

History
689 lines (559 loc) · 17.2 KB

File metadata and controls

689 lines (559 loc) · 17.2 KB

macOS Embedding for GoFlow

How Flutter embeds into macOS (and how GoFlow will do the same).

TL;DR

Flutter's approach:

  1. Native Swift app shell in macos/ directory
  2. Dart code stays in lib/ directory (separate!)
  3. Build process compiles Dart → App.framework
  4. Native shell loads Flutter.framework + App.framework at runtime
  5. No Dart code in macos/ - it's all configuration and native wrapper

GoFlow will do the same:

  • Native Swift shell in macos/
  • Go code in lib/
  • Build compiles Go → libapp.dylib
  • Shell loads GoFlowRuntime.framework + libapp.dylib
  • Clean separation, proper .app bundles

Flutter's macOS Structure

demo_flutter_for_template/
├── lib/
│   └── main.dart          # ← Dart code lives here!
│
├── macos/                 # ← No Dart code here!
│   ├── Runner/
│   │   ├── AppDelegate.swift           # App entry
│   │   ├── MainFlutterWindow.swift     # Window setup
│   │   ├── Info.plist
│   │   └── Assets.xcassets
│   │
│   ├── Flutter/
│   │   ├── Flutter-Debug.xcconfig      # #include ephemeral
│   │   ├── Flutter-Release.xcconfig    # #include ephemeral
│   │   ├── GeneratedPluginRegistrant.swift
│   │   └── ephemeral/                  # Auto-generated
│   │       └── Flutter-Generated.xcconfig
│   │
│   └── Runner.xcodeproj/
│
└── build/                 # Build output
    └── macos/
        └── Build/Products/Release/
            └── demo_flutter_for_template.app/
                ├── Contents/
                │   ├── MacOS/Runner      # Native exe
                │   └── Frameworks/
                       ├── FlutterMacOS.framework  # Engine
                       └── App.framework           # Dart code!

How Flutter's macOS Embedding Works

1. AppDelegate.swift - Entry Point

import Cocoa
import FlutterMacOS

@main
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}

What happens:

  • Inherits from FlutterAppDelegate (provided by FlutterMacOS.framework)
  • FlutterAppDelegate handles all Flutter initialization
  • No manual Flutter engine setup needed

2. MainFlutterWindow.swift - Window Setup

import Cocoa
import FlutterMacOS

class MainFlutterWindow: NSWindow {
  override func awakeFromNib() {
    // Create Flutter view controller
    let flutterViewController = FlutterViewController()

    // Set as window content
    let windowFrame = self.frame
    self.contentViewController = flutterViewController
    self.setFrame(windowFrame, display: true)

    // Register plugins
    RegisterGeneratedPlugins(registry: flutterViewController)

    super.awakeFromNib()
  }
}

What happens:

  • FlutterViewController is provided by FlutterMacOS.framework
  • It automatically:
    • Loads lib/main.dart (path from xcconfig)
    • Starts Dart VM
    • Renders Flutter UI
    • Handles events

3. Flutter-Generated.xcconfig - Build Configuration

// Auto-generated by Flutter build
FLUTTER_ROOT=/Users/user/flutter
FLUTTER_APPLICATION_PATH=/path/to/project
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1

What it does:

  • Tells Xcode where Flutter is installed
  • Specifies project path
  • Build metadata

4. Build Process

# flutter build macos

Step 1: Compile Dart code
  lib/main.dart → App.framework

Step 2: Run Xcode build
  xcodebuild -project Runner.xcodeproj

Step 3: Xcode links frameworks
  - FlutterMacOS.framework (engine)
  - App.framework (your code)

Step 4: Create .app bundle
  Runner.app/
    Contents/
      MacOS/Runner
      Frameworks/
        FlutterMacOS.framework
        App.framework  ← All Dart code is here!

GoFlow macOS Structure (Proposed)

mygoflowapp/
├── lib/
│   └── main.go            # ← Go code lives here!
│
├── macos/                 # ← No Go code here!
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── MainGoFlowWindow.swift
│   │   ├── Info.plist
│   │   └── Assets.xcassets
│   │
│   ├── GoFlow/
│   │   ├── GoFlow-Debug.xcconfig
│   │   ├── GoFlow-Release.xcconfig
│   │   └── ephemeral/
│   │       └── GoFlow-Generated.xcconfig
│   │
│   └── Runner.xcodeproj/
│
└── build/
    └── macos/
        └── Build/Products/Release/
            └── mygoflowapp.app/
                ├── Contents/
                │   ├── MacOS/Runner
                │   └── Frameworks/
                       ├── GoFlowRuntime.framework  # Our engine
                       └── libapp.dylib             # Go code!

GoFlow Implementation

1. GoFlow AppDelegate.swift

import Cocoa
import GoFlowRuntime

@main
class AppDelegate: NSObject, NSApplicationDelegate {
    var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Load main window from storyboard
        // Window will be configured by MainGoFlowWindow
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }

    func applicationWillTerminate(_ notification: Notification) {
        // Cleanup GoFlow runtime
        GoFlowRuntime.shutdown()
    }
}

2. MainGoFlowWindow.swift

import Cocoa
import GoFlowRuntime

class MainGoFlowWindow: NSWindow {
    var goflowViewController: GoFlowViewController!

    override func awakeFromNib() {
        // Create GoFlow view controller
        goflowViewController = GoFlowViewController()

        // Configure window
        let windowFrame = self.frame
        self.contentViewController = goflowViewController
        self.setFrame(windowFrame, display: true)

        // Load Go app
        // Path to libapp.dylib is determined by build system
        goflowViewController.loadApp()

        super.awakeFromNib()
    }
}

3. GoFlowRuntime.framework Structure

GoFlowRuntime.framework/
├── Versions/
│   └── A/
│       ├── GoFlowRuntime          # Compiled framework
│       ├── Headers/
│       │   ├── GoFlowRuntime.h
│       │   ├── GoFlowViewController.h
│       │   └── GoFlowView.h
│       └── Resources/
│           └── Info.plist
└── GoFlowRuntime -> Versions/Current/GoFlowRuntime

Framework provides:

// Public API
public class GoFlowViewController: NSViewController {
    public func loadApp()
    public func reload()
    public func handleEvent(_ event: NSEvent)
}

public class GoFlowView: NSView {
    // Renders Go UI
}

public class GoFlowRuntime {
    public static func initialize()
    public static func shutdown()
}

4. GoFlow-Generated.xcconfig

// Generated by: goflow build macos
GOFLOW_ROOT=/usr/local/goflow
GOFLOW_APP_PATH=$(PROJECT_DIR)/lib/main.go
GOFLOW_BUILD_DIR=build
GOFLOW_APP_NAME=mygoflowapp
GOFLOW_BUILD_NUMBER=1
GOFLOW_VERSION=0.2.0

// Framework paths
GOFLOW_RUNTIME_PATH=$(GOFLOW_ROOT)/runtime/macos/GoFlowRuntime.framework
GOFLOW_APP_DYLIB=$(BUILT_PRODUCTS_DIR)/libapp.dylib

// Linking
FRAMEWORK_SEARCH_PATHS=$(inherited) $(GOFLOW_ROOT)/runtime/macos
LD_RUNPATH_SEARCH_PATHS=$(inherited) @executable_path/../Frameworks

5. Build Process

# goflow build macos

Step 1: Compile Go code to shared library
  go build -buildmode=c-shared \
    -ldflags="-s -w" \
    -o build/macos/libapp.dylib \
    lib/main.go

Step 2: Generate xcconfig
  echo "GOFLOW_APP_DYLIB=libapp.dylib" > macos/GoFlow/ephemeral/GoFlow-Generated.xcconfig

Step 3: Run Xcode build
  xcodebuild \
    -project macos/Runner.xcodeproj \
    -scheme Runner \
    -configuration Release \
    OBJROOT=build/macos/obj \
    SYMROOT=build/macos/sym

Step 4: Copy frameworks and dylib
  cp build/macos/libapp.dylib \
     build/macos/Build/Products/Release/Runner.app/Contents/Frameworks/

Step 5: Sign and create .app
  codesign -s - --deep \
    build/macos/Build/Products/Release/Runner.app

Go Shared Library (libapp.dylib)

lib/main.go

package main

import "C"
import (
    "github.com/base-go/GoFlow/pkg/core"
    "github.com/base-go/GoFlow/pkg/widgets"
)

// Export for CGo - called by GoFlowRuntime.framework
//export GoFlowMain
func GoFlowMain() {
    app := core.NewApp()
    app.Run(func() core.Widget {
        return widgets.MaterialApp(
            home: MyHomePage(),
        )
    })
}

//export GoFlowHandleEvent
func GoFlowHandleEvent(eventType C.int, data unsafe.Pointer) {
    // Handle events from Swift
}

func main() {} // Required for c-shared

type MyHomePage struct {
    widgets.StatefulWidget
}

func (h *MyHomePage) CreateState() core.State {
    return &MyHomePageState{}
}

type MyHomePageState struct {
    widgets.State
    counter int
}

func (s *MyHomePageState) Build(ctx core.BuildContext) core.Widget {
    return widgets.Scaffold(
        appBar: widgets.AppBar(
            title: widgets.Text("GoFlow Demo"),
        ),
        body: widgets.Center(
            child: widgets.Column(
                children: []core.Widget{
                    widgets.Text("You have pushed the button this many times:"),
                    widgets.Text(
                        strconv.Itoa(s.counter),
                        style: core.TextStyle{FontSize: 32},
                    ),
                },
            ),
        ),
        floatingActionButton: widgets.FloatingActionButton(
            onPressed: func() {
                s.SetState(func() {
                    s.counter++
                })
            },
            child: widgets.Icon(Icons.Add),
        ),
    )
}

GoFlowRuntime.framework Implementation

GoFlowViewController.swift

import Cocoa
import Metal

public class GoFlowViewController: NSViewController {
    private var goflowView: GoFlowView!
    private var appHandle: UnsafeMutableRawPointer?

    public override func loadView() {
        goflowView = GoFlowView(frame: NSRect(x: 0, y: 0, width: 800, height: 600))
        self.view = goflowView
    }

    public override func viewDidLoad() {
        super.viewDidLoad()
        loadApp()
    }

    public func loadApp() {
        // Load the Go shared library
        guard let appPath = Bundle.main.path(forResource: "libapp", ofType: "dylib") else {
            print("Error: libapp.dylib not found")
            return
        }

        // Open shared library
        guard let handle = dlopen(appPath, RTLD_LAZY) else {
            print("Error loading libapp.dylib")
            return
        }
        appHandle = handle

        // Get GoFlowMain symbol
        guard let mainFunc = dlsym(handle, "GoFlowMain") else {
            print("Error: GoFlowMain symbol not found")
            return
        }

        // Call Go main function
        typealias GoFlowMainType = @convention(c) () -> Void
        let goMain = unsafeBitCast(mainFunc, to: GoFlowMainType.self)
        goMain()

        // Start render loop
        goflowView.startRenderLoop()
    }

    public func reload() {
        // For hot reload: reload the dylib
        if let handle = appHandle {
            dlclose(handle)
        }
        loadApp()
    }

    deinit {
        if let handle = appHandle {
            dlclose(handle)
        }
    }
}

GoFlowView.swift

import Cocoa
import Metal

public class GoFlowView: NSView {
    private var metalDevice: MTLDevice!
    private var metalLayer: CAMetalLayer!
    private var displayLink: CVDisplayLink?

    public override init(frame: NSRect) {
        super.init(frame: frame)
        setupMetal()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupMetal()
    }

    private func setupMetal() {
        // Setup Metal rendering
        metalDevice = MTLCreateSystemDefaultDevice()
        metalLayer = CAMetalLayer()
        metalLayer.device = metalDevice
        metalLayer.pixelFormat = .bgra8Unorm
        self.layer = metalLayer
        self.wantsLayer = true
    }

    public func startRenderLoop() {
        // Create display link for 60fps rendering
        CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)

        if let displayLink = displayLink {
            CVDisplayLinkSetOutputCallback(displayLink, { (displayLink, inNow, inOutputTime, flagsIn, flagsOut, context) -> CVReturn in
                // Call Go rendering function
                autoreleasepool {
                    self.render()
                }
                return kCVReturnSuccess
            }, Unmanaged.passUnretained(self).toOpaque())

            CVDisplayLinkStart(displayLink)
        }
    }

    private func render() {
        // Get drawable from Metal layer
        guard let drawable = metalLayer.nextDrawable() else { return }

        // Call Go render function (from libapp.dylib)
        // GoFlowRender() will draw the UI

        drawable.present()
    }
}

Complete Build Command Flow

goflow new myapp

myapp/
├── lib/
│   └── main.go          # Default app template
├── macos/
│   ├── Runner/
│   │   ├── AppDelegate.swift
│   │   ├── MainGoFlowWindow.swift
│   │   ├── Info.plist
│   │   ├── Assets.xcassets/
│   │   └── Base.lproj/
│   │       └── MainMenu.storyboard
│   ├── GoFlow/
│   │   ├── GoFlow-Debug.xcconfig
│   │   └── GoFlow-Release.xcconfig
│   └── Runner.xcodeproj/
└── go.mod

goflow build macos

#!/bin/bash
# Executed when user runs: goflow build macos

# 1. Create build directories
mkdir -p build/macos/Frameworks

# 2. Compile Go app to shared library
echo "Compiling Go code..."
go build \
  -buildmode=c-shared \
  -ldflags="-s -w" \
  -o build/macos/Frameworks/libapp.dylib \
  lib/main.go

# 3. Generate xcconfig
cat > macos/GoFlow/ephemeral/GoFlow-Generated.xcconfig <<EOF
GOFLOW_ROOT=/usr/local/goflow
GOFLOW_APP_DYLIB=\$(BUILT_PRODUCTS_DIR)/../../../Frameworks/libapp.dylib
GOFLOW_RUNTIME_PATH=\$(GOFLOW_ROOT)/runtime/macos
FRAMEWORK_SEARCH_PATHS=\$(inherited) \$(GOFLOW_RUNTIME_PATH)
EOF

# 4. Build with Xcode
echo "Building macOS app..."
xcodebuild \
  -project macos/Runner.xcodeproj \
  -scheme Runner \
  -configuration Release \
  -derivedDataPath build/macos

# 5. Copy dylib to app bundle
cp build/macos/Frameworks/libapp.dylib \
   build/macos/Build/Products/Release/Runner.app/Contents/Frameworks/

# 6. Code sign
codesign -s - --deep --force \
  build/macos/Build/Products/Release/Runner.app

echo "✓ Build complete: build/macos/Build/Products/Release/Runner.app"

Result

build/macos/Build/Products/Release/
└── myapp.app/
    └── Contents/
        ├── Info.plist
        ├── MacOS/
        │   └── Runner              # Native Swift executable
        ├── Frameworks/
        │   ├── GoFlowRuntime.framework  # Our runtime engine
        │   └── libapp.dylib            # Your Go code!
        └── Resources/
            └── Assets.car

Double-click myapp.app → Runs your Go GUI app!

Hot Reload Implementation

Since Go is compiled to a shared library (.dylib), hot reload is straightforward:

class GoFlowViewController {
    func reload() {
        // 1. Close current dylib
        if let handle = appHandle {
            dlclose(handle)
        }

        // 2. Recompile Go code
        // goflow triggers: go build -buildmode=c-shared lib/main.go

        // 3. Reload dylib
        appHandle = dlopen(newDylibPath, RTLD_LAZY)

        // 4. Call GoFlowMain again
        let mainFunc = dlsym(appHandle, "GoFlowMain")
        unsafeBitCast(mainFunc, to: GoFlowMainType.self)()

        // 5. Trigger redraw
        goflowView.setNeedsDisplay()
    }
}

Benefits of This Approach

✅ Clean Separation

  • Swift/macOS code in macos/
  • Go app code in lib/
  • No mixing of languages in same directory

✅ Standard macOS App

  • Creates proper .app bundle
  • Works with Xcode
  • Can submit to Mac App Store
  • Follows Apple guidelines

✅ Hot Reload

  • Reload .dylib without restarting app
  • Fast iteration
  • State preservation possible

✅ IDE Integration

  • Open macos/Runner.xcodeproj in Xcode
  • Build/run/debug from Xcode
  • Or use goflow run macos

✅ Distribution

  • Standard .app bundle
  • Code signing works
  • Notarization compatible
  • DMG creation standard

Next Steps

  1. Create GoFlowRuntime.framework

    • Window management
    • Metal rendering
    • Event handling
    • CGo bridge to Go code
  2. Template for macos/ directory

    • AppDelegate.swift
    • MainGoFlowWindow.swift
    • Xcode project
    • Storyboards
  3. Build script integration

    • goflow new generates macos/
    • goflow build macos compiles and links
    • goflow run macos launches app
  4. Hot reload

    • File watcher
    • Recompile Go → .dylib
    • Reload in running app

Summary: Flutter's macOS folder is just a thin native wrapper that loads the Flutter framework and compiled Dart code at runtime. GoFlow can do exactly the same with a Swift wrapper loading our runtime and compiled Go code. This gives us proper macOS integration, .app bundles, and hot reload.