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,SInt32maps toIntegerlongmaps toInt64(on 64-bit)floatmaps toSingledoublemaps toDoubleBOOLmaps toBooleanchar *maps toCStringNSString *maps toCFStringRefvoid *maps toPtrNSIntegermaps toIntegervoid(return) -- omit return type, useSubform
Windows (Win32 types):
BOOLmaps toInteger(Win32 BOOL is int, not a real boolean)HWND,HANDLEmaps toIntegerDWORDmaps toUInt32LPCTSTR,LPCWSTRmaps toWStringLPSTR,LPCSTRmaps toCStringINT_PTR,LONG_PTRmaps toInt64(on 64-bit)LPVOIDmaps toPtr
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 TargetMacOSblock. 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:
CStringfor the path,CStringfor the attribute name,Integerfor 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.