Xojo's framework is comprehensive. For most of what a desktop application needs to do, it's enough. File I/O, networking, database access, UI controls, printing, graphics -- the framework covers a lot of ground, and covers it well.

But every platform exposes capabilities that Xojo doesn't wrap. macOS has thousands of Cocoa and Core frameworks. Windows has the Win32 API and COM. Linux has POSIX and platform-specific libraries. When you need something the framework doesn't provide -- a specific system behavior, a native UI element, a low-level capability -- declares are how you reach for it.

Used well, declares are a sharp tool. Used carelessly, they're a maintenance burden that follows your project for years.

This post is about using them well.

What a Declare Is

A Declare statement tells Xojo that a function exists in an external library -- an OS framework, a system DLL, a shared library -- and describes its signature so Xojo can call it correctly.

Here's the simplest possible example. On macOS, the Foundation framework exposes NSTemporaryDirectory, which returns the path to the system's temporary directory. Xojo's SpecialFolder.Temporary covers this for most cases, but it illustrates the mechanics clearly:

// New API (Xojo 2019r2+)
// macOS only — must be inside a #If TargetMacOS block

#If TargetMacOS Then
  Declare Function NSTemporaryDirectory Lib "Foundation" () As CFStringRef
  Dim tempPath As String = NSTemporaryDirectory()
#EndIf

The Declare line specifies: a Function named NSTemporaryDirectory, found in the Foundation library, taking no parameters, returning a CFStringRef (Xojo's type for bridging Cocoa strings).

That's the pattern. Name the function, name the library, describe the parameters and return type, call it.

Before You Reach for a Declare

Stop. Check the framework first.

Declares are powerful, but they come with real costs:

Platform lock-in. A declare that calls a macOS API is dead code on Windows. A Win32 declare does nothing on macOS. Every declare you add narrows the portability of the code it lives in.

Maintenance burden. OS APIs evolve. Functions get deprecated. Library names change. A declare that worked on macOS 10.14 may behave differently on macOS 14. You own that maintenance.

Type mapping complexity. Getting data types wrong in a declare doesn't always produce a compile error -- it produces runtime corruption or a crash. The compiler can't validate your type annotations against the external library.

Third-party dependency. If you're calling into a non-system library, you're now responsible for its presence and version on every machine where your app runs.

The framework-native path should always be your first check. Not because declares are wrong, but because a framework solution is more portable, more maintainable, and less likely to fail in a future OS release.

When the framework genuinely falls short -- and sometimes it does -- declares are the right answer. Just know why you're reaching for them before you do.

Data Type Mapping: Getting It Right

The most failure-prone part of declares is type mapping. You're describing a foreign function interface to Xojo, and if your types don't match the actual library signature, you get silent data corruption or a crash.

Here are the most important mappings for common platforms.

macOS (Cocoa / C types):

  • int, SInt32 maps to Integer
  • long maps to Int64 (on 64-bit)
  • float maps to Single
  • double maps to Double
  • BOOL maps to Boolean
  • char * maps to CString
  • NSString * maps to CFStringRef
  • void * maps to Ptr
  • NSInteger maps to Integer
  • void (return) -- omit return type, use Sub form

Windows (Win32 types):

  • BOOL maps to Integer (Win32 BOOL is int, not a real boolean)
  • HWND, HANDLE maps to Integer
  • DWORD maps to UInt32
  • LPCTSTR, LPCWSTR maps to WString
  • LPSTR, LPCSTR maps to CString
  • INT_PTR, LONG_PTR maps to Int64 (on 64-bit)
  • LPVOID maps to Ptr

Two specific traps worth calling out:

Win32 BOOL is not a Xojo Boolean. Win32 uses an int where zero is false and anything non-zero is true. If you declare a Win32 function as returning Boolean, Xojo may misinterpret the value. Use Integer and test explicitly.

NSString and CFStringRef are bridged on macOS, but you must use CFStringRef -- not String -- in declare signatures. Xojo handles the conversion transparently when you assign the result to a String variable.

A Real Example: macOS File Quarantine

Here's a practical declare -- one you'd actually use in production code. On macOS, files downloaded from the internet get a quarantine flag. Some applications need to remove that flag programmatically (for legitimate reasons -- installers, update managers, tools that process downloaded content).

Xojo's framework doesn't expose this directly. A declare does:

// New API (Xojo 2019r2+)
// macOS only — remove the quarantine attribute from a downloaded file

#If TargetMacOS Then
  Sub RemoveQuarantineAttribute(f As FolderItem)
    If f = Nil Or Not f.Exists Then Return

    // removexattr removes an extended attribute by name
    // Parameters: path (CString), attribute name (CString), options (Integer)
    // Returns: 0 on success, -1 on failure
    Declare Function removexattr Lib "libc.dylib" _
      (path As CString, name As CString, options As Integer) As Integer

    Dim result As Integer = removexattr(f.NativePath, "com.apple.quarantine", 0)

    // result = 0 means success; result = -1 means failure (attribute may not exist)
    // Silently ignore -1 — the attribute simply wasn't present
  End Sub
#EndIf

This is what a well-written declare looks like in context:

  • It's inside a #If TargetMacOS block. It will never execute -- and never compile -- on other platforms.
  • The declare is defined inside the method, not at module scope. This is a valid Xojo pattern; declares can be local to the method that uses them.
  • The parameters are typed correctly: CString for the path, CString for the attribute name, Integer for the flags.
  • The return value is checked and the failure case is explicitly handled (and explained).

A Real Example: Windows System Information

On Windows, the Win32 API exposes GetSystemInfo for querying hardware details -- processor count, page size, architecture. Xojo doesn't wrap this directly.

// New API (Xojo 2019r2+)
// Windows only — query processor count via Win32

#If TargetWindows Then
  // SYSTEM_INFO structure — must match Win32 layout exactly
  Structure SYSTEM_INFO
    wProcessorArchitecture As UInt16
    wReserved As UInt16
    dwPageSize As UInt32
    lpMinimumApplicationAddress As Ptr
    lpMaximumApplicationAddress As Ptr
    dwActiveProcessorMask As UInt64
    dwNumberOfProcessors As UInt32
    dwProcessorType As UInt32
    dwAllocationGranularity As UInt32
    wProcessorLevel As UInt16
    wProcessorRevision As UInt16
  End Structure

  Function GetProcessorCount() As Integer
    Declare Sub GetSystemInfo Lib "Kernel32" (info As SYSTEM_INFO)

    Dim info As SYSTEM_INFO
    GetSystemInfo(info)
    Return info.dwNumberOfProcessors
  End Function
#EndIf

A few things worth noting here.

The Structure definition must exactly match the Win32 struct layout. Field order, field types, and field sizes must be byte-for-byte correct. If they're not, Xojo will read the wrong bytes and return garbage values. When working with Win32 structures, always verify the layout against the official Microsoft documentation.

GetSystemInfo returns void -- it takes a pointer to a struct and fills it in. In Xojo, you declare it as a Sub rather than a Function, and pass the struct by reference.

Cross-Platform Declares: The #If Pattern

If you need platform-specific behavior that requires a declare on each platform, the clean architecture is a single public method backed by platform-specific implementations:

// New API (Xojo 2019r2+)
// Cross-platform method with per-platform declare implementations

Function GetProcessorCount() As Integer
  #If TargetMacOS Or TargetLinux Then
    // POSIX sysconf — available on macOS and Linux
    Declare Function sysconf Lib "libc.dylib" (name As Integer) As Integer
    Const _SC_NPROCESSORS_ONLN = 58  // POSIX constant for online processor count
    Return sysconf(_SC_NPROCESSORS_ONLN)

  #ElseIf TargetWindows Then
    Structure SYSTEM_INFO
      wProcessorArchitecture As UInt16
      wReserved As UInt16
      dwPageSize As UInt32
      lpMinimumApplicationAddress As Ptr
      lpMaximumApplicationAddress As Ptr
      dwActiveProcessorMask As UInt64
      dwNumberOfProcessors As UInt32
      dwProcessorType As UInt32
      dwAllocationGranularity As UInt32
      wProcessorLevel As UInt16
      wProcessorRevision As UInt16
    End Structure

    Declare Sub GetSystemInfo Lib "Kernel32" (info As SYSTEM_INFO)
    Dim info As SYSTEM_INFO
    GetSystemInfo(info)
    Return info.dwNumberOfProcessors

  #Else
    Return 1  // Fallback for unsupported platforms
  #EndIf
End Function

The caller gets a clean Integer. They never see the platform complexity. This is the right abstraction: one interface, multiple implementations, all the #If logic contained.

Plugins vs. Declares: When to Choose What

Declares aren't the only way to access native APIs. Third-party plugins -- MBS, Einhugur, and others -- wrap many native capabilities in a Xojo-friendly API. Before writing a declare, it's worth knowing when a plugin is the better path.

When declares make sense: You write them yourself, you own the ongoing maintenance, and you handle type safety manually (get it right or you crash). They work with any native API and cost nothing. But you're responsible for platform support on each target.

When plugins make sense: You install and configure them, the vendor owns the maintenance, and type safety is already handled for you. They're often multi-platform out of the box. The tradeoff is a licensing fee and coverage limited to what the plugin provides.

In practice, I've found the decision comes down to scope. One function: write the declare. Ten functions across three platforms: evaluate the plugin. The plugin licensing cost is often less than the time you'd spend writing, testing, and maintaining the declares yourself.

The Crawl, Walk, Run Path

Crawl: Write your first declare for a specific, well-documented native API call. Keep it inside a #If block. Map the types carefully against the official platform documentation. Test it. Document what the declare does and why the framework doesn't cover it.

Walk: Build platform-specific behavior as encapsulated methods. The declare goes inside the method; the rest of your app calls the clean interface. Add the #Else fallback for platforms where the declare doesn't apply.

Run: Evaluate whether a plugin covers the capability better than a hand-written declare for complex or multi-function native API access. Treat declares as a maintenance liability to be managed -- document them, keep them isolated, and revisit them when you target a new OS version.

The Bottom Line

Declares are what you reach for when Xojo's framework runs out of road. They're not a workaround -- they're a legitimate tool for accessing platform capabilities that the framework doesn't expose.

But every declare is a contract between your code and a specific platform library. Get the types wrong and you get crashes. Skip the #If block and you get build failures on other platforms. Neglect the documentation and you get maintenance surprises when the OS changes.

Use them deliberately. Type them carefully. Isolate them behind clean interfaces. And check the framework first -- every time.

The native API is always there if you need it. The goal is to need it as rarely as possible, and use it as cleanly as possible when you do.