"Write once, run anywhere" is one of the most attractive promises in software development. It's also one of the most misunderstood.

Xojo delivers on the core of that promise better than most platforms. You write code in a single language, maintain a single codebase, and compile native executables for macOS, Windows, and Linux. That's real. It works. Thousands of developers ship production software this way.

But "write once" doesn't mean "think once." The developers who struggle with cross-platform Xojo development are almost always the ones who treated platform differences as an edge case rather than a design constraint. The developers who ship cleanly are the ones who designed for those differences from the beginning.

This post is about what cross-platform development in Xojo actually looks like, where it's seamless, where it requires attention, and how to build apps that feel native on every platform you target.

Where Xojo Handles It For You

Start with the good news. Xojo's framework abstracts a significant amount of platform complexity, and it does so reliably.

Core language features work identically everywhere. Strings, arrays, collections, classes, exceptions, file I/O through FolderItem and TextInputStream all behave consistently across macOS, Windows, and Linux. Write the logic once; it runs the same.

Standard controls render natively on each platform. A DesktopButton on macOS looks like an Aqua button. The same code on Windows renders a Windows button. You don't manage this. The framework does. Your UI looks appropriate on each platform without conditional code.

Database access through Xojo's database classes works the same on all desktop targets. Whether you're using SQLite, connecting to MySQL, or querying PostgreSQL, the API is consistent. Your queries, your prepared statements, your result set handling are all portable.

Networking through URLConnection is cross-platform. HTTP requests, SSL handling, response parsing: write it once.

This is meaningful. The majority of a well-structured Xojo application (the data model, the business logic, the database layer, the networking layer) is genuinely write-once.

Where Platform Differences Bite You

The places where cross-platform development requires conscious attention are predictable. Once you know them, you can design around them. The surprises only happen when you don't.

File Paths

FolderItem.NativePath returns the OS-native path format. On macOS and Linux, that's forward slashes. On Windows, that's backslashes. If you store a path as a string and try to use it on a different platform, it will break.

The fix is to never store raw path strings across platform boundaries. Use FolderItem objects for path manipulation and let the framework handle the formatting. If you must serialize a path (to a database, a preferences file, or a config) use FolderItem.URLPath, which produces a consistent format regardless of platform.

// Wrong: storing NativePath as a string for later use
Dim savedPath As String = f.NativePath  // breaks on other platforms

// Right: use URLPath for serialization
Dim savedPath As String = f.URLPath  // consistent across platforms

// Right: reconstruct from URLPath
Dim restored As FolderItem = FolderItem.URLPath(savedPath)

UI Conventions

Each platform has conventions your users expect. Violating them doesn't crash your app. It just makes it feel wrong.

The most common differences:

  • Menu bar location. On macOS, the menu bar lives at the top of the screen, not the top of the window. On Windows and Linux, it's attached to the window. Xojo handles menu rendering correctly per platform, but you need to structure your menus with this in mind. Don't hardcode assumptions about menu position in your layout logic.
  • Keyboard shortcuts. macOS uses Command; Windows and Linux use Control. Xojo maps these through the KeyboardShortcut properties on menu items, but verify your shortcuts make sense on each platform.
  • Dialog button order. On macOS, the affirmative action goes on the right ("Cancel" then "OK"). On Windows, it's reversed. Xojo's built-in dialogs handle this automatically. Custom dialogs don't. You'll need to handle it with #If blocks.
  • Window chrome. Title bars, resize handles, and close/minimize/maximize button placement differ between platforms. Don't position controls relative to window chrome in ways that assume a specific platform.

Fonts and Text Rendering

Font availability varies. A font that ships with macOS may not exist on Windows. If you specify font names directly, test on every target.

The safer path: use system fonts. Xojo provides access to the system default font, which renders correctly and appropriately on each platform without hardcoding a name.

// Risky: hardcoded font name
g.Font = "San Francisco"  // Only on macOS

// Safer: use the system font
g.Font = Font.SystemFont  // Correct and native on every platform

Declares

This is the most important platform constraint to understand. Declares let you call native OS APIs directly from Xojo. They are, by definition, platform-specific.

A declare that calls a macOS Carbon or Cocoa API does nothing on Windows. It won't compile for Windows unless you protect it. A declare that calls a Win32 API is dead code on macOS.

Any time you use a declare, you need a #If block:

// Platform-conditional declare

#If TargetMacOS Then
  Declare Function NSHomeDirectory Lib "Foundation" () As CFStringRef
  Dim homePath As String = NSHomeDirectory()
#ElseIf TargetWindows Then
  // Windows equivalent
  Dim homePath As String = Environ("USERPROFILE")
#ElseIf TargetLinux Then
  Dim homePath As String = Environ("HOME")
#EndIf

The presence of a declare in your code is a flag that says: this code has platform assumptions. Treat it accordingly.

Designing for Cross-Platform from the Start

The developers who run into cross-platform problems late in development are usually the ones who built on a single platform and tested on others afterward. By then, the assumptions are baked in.

The better approach is to design with platform awareness as a first-class constraint, not an afterthought.

Separate platform-specific code into dedicated methods. When you need platform-specific behavior, don't scatter #If blocks throughout your UI code. Write a method that encapsulates the behavior, implement it once per platform inside that method, and call the clean interface everywhere else.

// Clean interface — caller doesn't need to know about platform differences

Function GetUserHomeDirectory() As FolderItem
  #If TargetMacOS Then
    Return FolderItem.URLPath("file:///~")
  #ElseIf TargetWindows Then
    Dim path As String = Environ("USERPROFILE")
    Return New FolderItem(path, FolderItem.PathModes.Native)
  #ElseIf TargetLinux Then
    Dim path As String = Environ("HOME")
    Return New FolderItem(path, FolderItem.PathModes.Native)
  #EndIf
End Function

Test on all target platforms regularly. Not at the end of a release cycle. Regularly. A UI issue that takes five minutes to fix when you catch it on Tuesday takes two hours to untangle when you find it the week before you ship.

Use conditional compilation for UI adjustments, not workarounds. #If TargetMacOS is a legitimate tool for delivering a native experience on each platform. Using it to paper over a bug you don't fully understand is a different thing. Know the difference.

How the Three Platforms Diverge

Here's a practical summary of where macOS, Windows, and Linux differ in ways that matter for Xojo development.

File path separators: macOS and Linux use forward slashes (/). Windows uses backslashes (\). Always let FolderItem handle this for you.

Home directory access: On macOS, you'd typically use an NSHomeDirectory declare or the ~ shorthand. On Windows, it's the USERPROFILE environment variable. On Linux, it's the HOME environment variable.

Font rendering: macOS uses Core Text and is Retina-aware. Windows uses GDI+. Linux font rendering varies depending on the desktop environment. I've found that sticking with system fonts sidesteps most of the inconsistencies.

Menu bar placement: macOS places the menu bar at the screen level. Windows and Linux attach it to the window. Xojo handles this, but keep it in mind when designing your layouts.

Default button order: macOS puts the affirmative button on the right. Windows puts it on the left. Linux varies by desktop environment. Xojo's built-in dialogs respect these conventions; custom dialogs are on you.

Native dialogs: macOS uses NSOpenPanel and NSSavePanel. Windows uses Win32 dialogs. Linux uses GTK dialogs. Again, Xojo abstracts this, but be aware of it when you're using declares to customize dialog behavior.

Packaging: macOS apps are distributed as .app bundles, optionally wrapped in a .dmg. Windows uses .exe installers. Linux packaging varies widely.

Code signing: On macOS, code signing is required for Gatekeeper. On Windows, it's optional but affects SmartScreen warnings. On Linux, it's not required.

The Crawl, Walk, Run Path

Crawl: Build your first cross-platform app targeting one platform. Get the logic right, get the data layer right, get the UI functional. Don't optimize for cross-platform yet. Just keep platform assumptions out of your core logic.

Walk: Add a second target platform. Run the same codebase on it. Fix the file path issues, the font issues, the UI convention issues. Refactor the platform-specific pieces into dedicated methods. Add #If blocks where needed.

Run: Design all new features with three-platform testing as part of the definition of done. Use platform-specific code deliberately and label it clearly. Ship on all targets with confidence because you've verified the behavior, not assumed it.

The Honest Summary

Cross-platform development in Xojo is as close to "write once" as the industry offers for native desktop apps. The core of your application (the logic, the data, the networking) really is portable without modification.

The UI layer, the file system interaction, and any native API access require platform awareness. That's not a flaw in Xojo. That's the honest shape of cross-platform development on operating systems that were designed independently of each other.

Design for the differences. Test on all targets. Keep platform-specific code isolated and labeled.

Do that, and "write once" gets very close to true.