Skip to content

Fix and enable building FreeBASIC for Darwin / Apple Silicon - #479

Open
agorangetek wants to merge 9 commits into
freebasic:masterfrom
agorangetek:darwin-arm64-support
Open

agorangetek wants to merge 9 commits into
freebasic:masterfrom
agorangetek:darwin-arm64-support

Conversation

@agorangetek

@agorangetek agorangetek commented Sep 18, 2026

Copy link
Copy Markdown

Fix and enable building FreeBASIC for Darwin / Apple Silicon

Upstream publishes no macOS binaries of any version (the SourceForge
Binaries-* folders only ever contain DOS, Windows, Linux, FreeBSD and
Raspberry Pi packages), so fbc can currently only be obtained on macOS by
bootstrapping it by hand — and that fails, because the Darwin support has
bit-rotted against modern ld64 and against arm64. This branch makes a native
darwin-aarch64 build work end to end.

I bootstrapped from the official FreeBASIC-1.10.1-source-bootstrap package
(its linux-aarch64 compiler C sources are the closest CPU family; they are
self-contained C with no #includes) plus a libfb built natively from the
100%-C runtime sources, and then built current master with it. Getting there
needed the fixes below.

Commits

  1. darwin: define DISABLE_OPENGL when X11/XQuartz is unavailable
    gfx_opengl.c still calls fb_hGL_GetProcAddress(), which only the X11
    driver defines; without XQuartz libfbgfx.a has an undefined symbol and
    every link fails. The stub for this already exists, it just was not
    selected.

  2. rtl-error: don't emit an indirect jump when RESUME support is off
    rtlErrorCheck()/rtlErrorThrow() pass a resume label only when -ex/-exx
    enabled RESUME support; without it fb_ErrorThrowAt() can never return a
    usable jump target, so the goto *result is dead. Emitting a plain call
    instead is equivalent and additionally keeps the generated C acceptable to
    clang, which rejects goto *ptr in a function with no address-of-label
    expression. -ex/-exx code is unchanged.

  3. darwin: align DATA descriptors to the pointer size — DATA tables are
    { short, void* } packed, so the embedded pointers are 2-byte aligned.
    ELF tolerates that, ld64 does not: ld: pointer not aligned in '_label$N'+0x16, a hard error on arm64. Darwin now gets the naturally
    aligned descriptor (16 bytes); other targets keep the packed layout.

  4. darwin: use the correct arm64 va_list ABI — Apple's arm64 va_list
    is a plain pointer (sizeof(va_list) == 8), not the AAPCS64
    __va_list_tag struct; cva_start()/cva_arg() read garbage and any
    cva_list program crashed.

  5. profile_cycles: handle Mach-O sections — Mach-O has no
    __start_/__stop_ symbols and 16-character section names, so the ELF
    section scan cannot work; guard it for Darwin and degrade to an empty
    report instead of failing to build.

  6. darwin: link through the C compiler driver instead of driving ld64
    the Darwin link line was a GNU ld line (crt1.o/crti.o/crtbegin.o,
    -macosx_version_min 10.4, --eh-frame-hdr, -lgcc, -shared -h<name>).
    ld64 rejects --eh-frame-hdr, the crt objects are not linkable that way on
    macOS, and libgcc does not exist unless a GCC toolchain is installed.
    Darwin now links via clang, which brings in the startup object and
    libSystem; -dylib maps to -dynamiclib -install_name, and -arch arm64
    is passed explicitly.

  7. darwin: name shared libraries .dylib, and export their symbols
    -dylib named its output .so on every unix target, including Darwin, where
    Mach-O calls it .dylib; .so is the loadable-bundle extension. It worked
    only because fb_DylibLoad() falls through to the .so entry in its
    candidate list. The link line also dropped --export-dynamic for Darwin
    rather than translating it, so -export did nothing there; it is now
    -Wl,-export_dynamic.

Validation

Built master (5714d10) natively for darwin-aarch64 on macOS 27 / Apple M5
with the Xcode 27 toolchain, then ran the upstream fbcunit suite
(make unit-tests, 2283 cases, -exx -mt):

 Asserts    Passed    Failed  Suite
1151707   1151705         2  Total

tests/unix-tty — which builds a library with -dylib and loads it back
through fb_DylibLoad() — could not be built on macOS at all before this
branch (tester.cxx needs C++17, which Apple clang does not default to, and
compares the Linux-only termios.c_line), so nothing exercised that path on
Darwin. It builds now, and its two dylibload testees pass on Apple silicon:

testee-dylibload-only-examplelib          123 hello hello hello
testee-dylibload-dylibfree-examplelib     123 hello hello hello

The whole suite passes with both C backends (Apple clang and GCC 16); the
two failing assertions are the same in both and are the only remaining issue I
know of:

optimizations/consteval.bas(63) : CU_ASSERT_DOUBLE_EQUAL(acos(-1.0f), acos(f), EPSILON_SNG)

which looks like a constant-folding vs. runtime-libm ULP difference rather
than an arm64 regression — I could not check it on other platforms, so please
treat it as unverified.

fbc also rebuilds itself with Apple clang alone (no GCC anywhere), and
rebuilds itself with GCC; both binaries behave identically on the suite.

Reproducing

brew install gcc                     # for the bootstrap only
make -C fbc-1.10.1 rtlib TARGET=aarch64-darwin CC=clang AR=ar AS=as
make -f bootstrap-build.mk           # pre-generated C + native libfb -> stage-1 fbc
cp build/fbc-stage1 fbc-1.10.1/bin/fbc
cd fbc
make rtlib gfxlib2 TARGET=aarch64-darwin CC=clang AR=ar AS=as
GCC=/opt/homebrew/bin/gcc-16 CLANG=/usr/bin/clang AS=/usr/bin/as AR=/usr/bin/ar \
  make compiler TARGET=aarch64-darwin CC=clang AR=ar AS=as \
       FBC="../fbc-1.10.1/bin/fbc -i $PWD/inc" || true
# stage 1 still has the old link logic, so link the first Darwin fbc by hand:
clang -o bin/fbc lib/freebasic/darwin-aarch64/fbrt0.o \
      src/compiler/obj/darwin-aarch64/*.o \
      -Llib/freebasic/darwin-aarch64 -lfb -lncurses -lm
# from here bin/fbc has the fixed linking and can rebuild itself:
make compiler TARGET_OS=darwin TARGET_ARCH=aarch64 CC=clang AR=ar AS=as FBC="bin/fbc -i inc"

Relationship to other pull requests

  • The makefile change that normalizes arm64 to aarch64 is taken from
    Fix macOS arm64 (Apple Silicon) linker support #470 by @metaneutrons — thanks. It is the piece of that PR which makes a
    plain make work on Apple Silicon; without it uname -m's "arm64" is
    normalized to the 32-bit arm and everything lands in darwin-arm.
  • Fix macOS arm64 (Apple Silicon) linker support #470 stops short of linking, which may be worth knowing before choosing
    between them: it does not touch the crt1.o lookup, and on current macOS
    that lookup cannot resolve (clang --sysroot=<sdk> -print-file-name=crt1.o
    returns the bare name, so hFindLib() reports error 23: File not found, crt1.o), leaving ld64 with no startup object:
    Undefined symbols for architecture arm64: "_main". It also predates the
    DATA descriptor alignment problem. Both are fixed here.
  • Add support for Cocoa OpenGL screen in gfxlib2 and other compiler error fixes for macOS  #448 (Cocoa OpenGL) touches makefile, src/compiler/fbc.bas and
    src/rtlib/profile_cycles.c as well, so it will conflict with this branch.
    It is the better long-term answer for macOS graphics (no XQuartz needed);
    this branch only makes the existing X11/XQuartz path build and work. Happy
    to rebase on whichever lands first.

FB's OpenGL support is implemented in the X11 gfx driver.  Without
ENABLE_XQUARTZ that driver is compiled out, but gfx_opengl.c is still
built and calls fb_hGL_GetProcAddress(), which only the driver defines.
libfbgfx.a therefore ends up with an undefined symbol and every link
fails:

    Undefined symbols for architecture arm64:
      "_fb_hGL_GetProcAddress", referenced from:
          _fb_GfxGetGLProcAddress in libfbgfxmt.a[31](gfx_opengl.o)

gfx_opengl.c already provides a stub for exactly this situation; define
DISABLE_OPENGL alongside DISABLE_X11 so it is used.
rtlErrorCheck() and rtlErrorThrow() only have a resume label to hand to
the error throw when -ex/-exx enabled RESUME support.  Without it the
label arguments are NULL, fb_ErrorThrowAt() can never return a usable
jump target, and `fb_ErrorThrow(); goto *result;` is equivalent to a
plain call.

Emitting the call also keeps the generated C compilable by clang, which
rejects `goto *ptr;` in a function that contains no address-of-label
expression ("indirect goto in function with no address-of-label
expressions").

Code built with -ex/-exx is unaffected: there the label addresses are
taken anyway (&&label), so the indirect jump is still emitted.
DATA items are emitted as a packed { short, void* } table, 10 bytes per
entry, so the embedded string/link pointers sit at 2-byte offsets.
ELF linkers accept that, but Mach-O's ld64 refuses to relocate a
pointer that is not pointer-aligned and fails the link:

    ld: pointer not aligned in '_label$N'+0x16

Give the descriptor the natural pointer alignment when targeting
Darwin, and make fb_data.h use the matching, non-packed layout.  Other
targets keep the packed 10-byte layout so their ABI is unchanged.
AArch64 Darwin uses a plain pointer-like va_list (sizeof(va_list) == 8),
not the AAPCS64 __va_list_tag struct used by Linux/BSD aarch64.  The
compiler assumed the struct, so cva_start()/cva_arg() walked the wrong
memory and any program using cva_list crashed immediately.
Mach-O has no __start_/__stop_ section boundary symbols and limits
section names to 16 characters, so the ELF-style section scan cannot
work there.  Guard it for Darwin and fall back to the single version
record (the report is then simply empty) instead of emitting an invalid
section attribute and failing to build.
The Darwin link command was built for a GNU ld: it adds crt1.o/crti.o/
crtbegin.o/crtend.o/crtn.o, `-macosx_version_min 10.4`, `--eh-frame-hdr`,
`--export-dynamic` and `-lgcc`, and uses `-shared -h<name>` for shared
libraries.  Modern ld64 rejects --eh-frame-hdr outright, the crt objects
cannot be linked that way on macOS, and libgcc does not exist unless a
GCC toolchain happens to be installed.

Invoke the C compiler driver (clang) for linking Darwin targets instead:
it supplies the startup object and libSystem itself.  Drop the options
it does not accept, use -dynamiclib/-install_name for -dylib, and add
-arch arm64 so the architecture is explicit.

Executables and dynamic libraries now link with the system toolchain
alone.
Xfuncproto.h defines NeedWidePrototypes to 1 unless the application
defines NARROWPROTO, which makes Xlib declare XGetKeyboardMapping()'s
first_keycode parameter as unsigned int instead of KeyCode (unsigned
char).  FB's XGETKEYBOARDMAPPING typedef hard-codes KeyCode, so passing
XGetKeyboardMapping to fb_hInitX11KeycodeToScancodeTb() no longer
matches the parameter type:

    gfx_x11.c:588:68: error: incompatible function pointer types
      passing 'KeySym *(Display *, unsigned int, int, int *)' to
      parameter of type 'XGETKEYBOARDMAPPING' (aka
      'unsigned long *(*)(struct _XDisplay *, unsigned char, int, int *)')

GCC only warns about the mismatch, which is why this went unnoticed, but
clang (and GCC 14+, where it is an error too) rejects it -- so the X11
rtlib/gfxlib2 code cannot be built by clang at all.  That makes the
XQuartz/X11 graphics path unbuildable on Darwin, where clang is the
system compiler.

Mirror Xlib's own conditional in the typedef so it matches either way.
uname -m reports "arm64" on Apple Silicon, which matched the arm% pattern
and was normalized to "arm" -- the 32-bit family.  A plain "make" on macOS
therefore resolved FBTARGET to darwin-arm instead of darwin-aarch64, so
objects and libraries landed in directories that do not match the ones fbc
looks for at run time (lib/freebasic/darwin-aarch64).

Normalize aarch64/arm64 first, then let the remaining 32-bit spellings
match armv%/arm.  Verified that arm-linux-gnueabihf and
armv7-linux-gnueabihf still resolve to linux-arm, and that
arm64-apple-darwin resolves to darwin-aarch64.

Based on the corresponding change in freebasic#470 by @metaneutrons.
metaneutrons added a commit to metaneutrons/freebasic-ng that referenced this pull request Sep 19, 2026
## Summary

Seven defects on the Darwin and clang paths, derived from reviewing
upstream
freebasic/fbc#479 against this tree. The assembly backends keep their
current
behaviour throughout.

- **DATA descriptors are not linkable on Darwin.** The descriptor is
emitted
packed, which puts its pointer at offset 2 and every 10 bytes after
that.
ld64 requires a pointer relocation to be pointer-aligned and rejects
that,
fatally on arm64, so any program containing a `DATA` statement fails to
link
on Apple silicon. Darwin now uses the natural layout on both the
compiler and
  the runtime side.

- **`fbc -e` cannot be compiled by clang.** The C backend emits the jump
to the
error handler as a computed goto, which clang rejects in a function that
takes
  no label address — every procedure containing an error check is such a
function when RESUME support is off. This hit macOS and, as the new
regression
showed, the Windows aarch64 host as well, whose CLANGARM64 toolchain
ships
clang under the gcc name. The former workaround, one unused
address-taken
label per procedure, is worse than the error it hid: with only one
possible
destination both clang and gcc fold the jump into a direct branch to
that
label, so `-gen clang -e -O 2` loops forever instead of reaching the
handler.
The C backend now emits the resume labels even without `-ex`, which is
the
  shape `-ex` already produces.

- **`fbc -dylib` cannot link on macOS.** The shared-library path passed
the GNU
spellings `-shared`, `-h<soname>` and `--export-dynamic` to the clang
driver,
which rejects `-h<soname>`. Darwin now uses `-dynamiclib` with
`-install_name`
  and `-Wl,-export_dynamic`.

- **Darwin shared libraries were named `.so`.** Mach-O uses `.dylib`;
`.so` is
the loadable-bundle extension, and `fb_DylibLoad()` tries the `.dylib`
name
  first on Darwin.

- **OpenGL was enabled without an X11 driver.**
`gfx_driver_opengl_x11.c` is the
  only unix translation unit defining `fb_hGL_GetProcAddress()`, while
`gfx_opengl.c` calls it unconditionally, so libfbgfx kept an undefined
symbol
that surfaced at user link time. macOS without XQuartz hits this by
default
because the SDK supplies `OpenGL/gl.h`. The graphics library also
decided
about X11 independently of the runtime, which made a machine with
XQuartz
  installed build an X11 driver against a runtime built without X11.

- **`XGetKeyboardMapping` prototype mismatch.** Xlib declares
`first_keycode` as
`KeyCode` or `unsigned int` depending on `NeedWidePrototypes`; the
runtime
typedef hard-coded the narrow form. Clang treats the mismatch as an
error,
  GCC only warns.

- **The compiler regressions could not run on Windows.** MSYS Python
reads a
`C:/...` argument as a relative POSIX path, so the adapter was looked up
below
the build directory. Registration now goes through one helper that hands
Windows relative paths and plain tool names, the way the legacy suites
already
  do, and every regression runs with the build directory as its working
  directory.

Three portable regressions are added: `compiler.data-statement`,
`compiler.error-check-handler` (built with `-e`) and
`compiler.dylib-load`,
which builds a library with `-dylib` and loads it back through
`DyLibLoad()`.
`scripts/run-compiler-regression.py` gained `--fbc-flag` and
`--library-source`,
resolves a tool name through PATH and also accepts `.exe` names.

## Verification

- [x] CMake build completed on the affected host (Linux x86_64, seed and
      self-hosted builds; CI covers the Darwin and Windows hosts).
- [x] `fbc --version` or a focused compiler/runtime test passed — the
three new
regressions plus the full legacy fbcunit suite, green both with `-g
-exx`
      (2302 tests, ~1.15M asserts) and without it.
- [x] Copyright and licence notices remain intact.
- [x] No generated binaries or unrelated changes are included.

Evidence collected while fixing these:

- The DATA layout was checked against the object file: with the packed
descriptor `clang -target arm64-apple-macos11` emits
`ARM64_RELOC_UNSIGNED`
  at offsets 2, 0xc and 0x16, and at 8, 0x18 and 0x28 after the change.
- The error path was checked at `-O 0`, `-O 2` and `-O 3` with `-gen
clang`;
  the previous compiler hangs at `-O 2` and `-O 3`, this one reaches the
  handler at all three.
- The Darwin link lines were checked by cross-targeting with a stub
toolchain:
  `-arch x86_64 -o libfoo.dylib -dynamiclib -install_name libfoo.dylib
  -Wl,-export_dynamic ...`.

`Self-host Darwin ARM64` runs the three regressions on Apple silicon, so
the
DATA layout, `-e` under clang and the `-dynamiclib` link line are now
measured
on the target rather than derived.

Unrelated defect found along the way and filed separately: #159.
Two defects on the -dylib path, both found while cross-checking metaneutrons'
freebasic-ng#160, which reviewed this branch against their fork:

- Shared libraries were named .so on every unix target, including Darwin.
  Mach-O uses .dylib -- .so is the loadable bundle extension. fb_DylibLoad()
  looks for the .dylib name first on Darwin, so it worked, but only by falling
  through to the .so entry in its candidate list, and the file was named wrongly
  for every other tool on the platform. Darwin now has its own case in the
  output naming, leaving the ELF targets on .so.

- The link line skipped --export-dynamic on Darwin entirely rather than
  translating it. The clang driver wants it spelled -Wl,-export_dynamic.
  Symbols resolved without it -- Mach-O exports global symbols by default --
  but leaving it out was a silent difference from every other unix target, and
  -export therefore did nothing on Darwin.

Verified by building a library and loading it back:

    fbc -dylib foo.bas                -> libfoo.dylib, install name "libfoo"
    linking: clang ... -dynamiclib -install_name "libfoo" -Wl,-export_dynamic
    DyLibLoad("foo") -> handle, DyLibSymbol(handle, "FOO_ADD") -> 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant