3 Commits

Author SHA1 Message Date
CHEN Feng 86f175c3c4 Add macOS (darwin/arm64) build support (#158)
* feat(macos): build flare on macOS (darwin/arm64) + thirdparty alignment & bug fixes

Make flare build with both blade and bazel on macOS (Apple Silicon,
darwin/arm64); previously Linux-only. Verified: `blade build flare/...` and
`bazel build //flare/...` (653 targets) both green on macOS arm64.

Third-party / build system:
- Align bazel deps with the verified blade build: glog 0.4.0->0.7.1,
  zlib 1.2.13->1.3.2, openssl 1.0.2u->1.1.1w (1.1 required by HMAC_CTX usage).
- glog: extend the export-guard patch to every header gating <glog/export.h>
  behind GLOG_USE_GLOG_EXPORT, and update CHECK_OP to use
  google::logging::internal::{Make,}CheckOpString.
- foreign_cc macOS fixes: force a real `ar` (rules_foreign_cc defaults AR to
  Apple's libtool, incompatible with autotools' ar-style calls); gate -lrt to
  Linux; pick .dylib vs .so; build nghttp2 static-only (its sandbox dylib
  install_name breaks curl's configure run-time check).
- Register the host preinstalled make toolchain (rules_foreign_cc's bundled
  make segfaults building jemalloc on macOS; blade also uses the system make).
- Track patch files as unpack-rule inputs (foreign_build.bld).
- Silence third-party warnings (external/ and in-repo thirdparty/) via
  per_file_copt.

flare portability:
- IO: new Poller abstraction with epoll (Linux) and kqueue (macOS) backends;
  event_loop/descriptor rewired off raw epoll; eventfd -> pipe notifier.
- net/thread/cpu/rlimit platform guards; OpenSSL 1.1 HMAC_CTX API.
- Add missing `gflags/gflags.h` / `<unistd.h>` includes (were pulled in
  transitively by the old glog).
- gperftools/tcmalloc is Linux-only: excluded on darwin (mirrors blade);
  macOS uses jemalloc.

Bug fixes (also affect Linux):
- base/expected.h: Unexpected::swap referenced a nonexistent member
  (other.unex -> other.unex_); never compiled once instantiated.
- io/detail/writing_buffer_list.cc: FlushTo crashed (CHECK) on a benign drain
  race (head_ stale while tail_ == nullptr); now treats the list as empty.
- base/chrono.{h,cc}: clocks delegated to a hand-rolled clock_gettime that
  mixed clock epochs (broke TimerWorker::wait_until); now use
  std::chrono::*::now(); ReadCoarseSystemClock duration_casts to the clock's
  duration.
- fiber/detail/scheduling_group.cc: spin-wait bound was a hardcoded TSC-tick
  count assuming a GHz-scale TSC; now derived from the measured TSC frequency.
- base/internal/dpc.cc: "Run N DPCs" logged cbs.size() (always 0 at loop exit)
  instead of the accumulated dpcs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fiber: defeat Clang's TLS-base CSE across returns_twice on AArch64

On AArch64, Clang caches the resolved thread_local slot address in a
callee-saved register and reuses it across `[[gnu::returns_twice]]`
calls. `jump_context` may resume the same C++ frame on a different OS
thread, so the cached slot points to the previous worker's TLS region
on resume, and accesses go to the wrong memory.

GCC handles `returns_twice` more conservatively (it re-issues
`mrs tpidr_el0` after the call), which is why Linux flare worked
inlined for years -- Linux + GCC, not "Linux".

Centralize the canonical `jump_context` declaration in a new
`context.h` (so every TU sees the `returns_twice` attribute) and
force the three TLS accessors out-of-line: `[[gnu::noinline]]` makes
each access an ABI call, and ABI rules force the caller to redo the
TLS resolution -- Clang can't CSE across a real function boundary.

Discovered while porting flare to macOS arm64 (Apple Clang), but the
underlying hazard exists on any Clang aarch64 build; GCC users were
just lucky.

* io/kqueue: fix Modify to disable removed filters; stop fanning EV_EOF to opposite direction

Two macOS-port correctness fixes in the kqueue poller, both surfaced
by a flare RPC client that died with STATUS_IO_ERROR on the first
write to a freshly-opened connection.

1. `Modify` now explicitly EV_DISABLEs filters that aren't in the
   new event mask. Unlike Linux epoll (EPOLL_CTL_MOD replaces the
   whole event bitmask in one shot), kqueue registers EVFILT_READ
   and EVFILT_WRITE independently -- a bare EV_ADD on one filter
   does NOT affect the other. Without explicit disable, the kernel
   keeps firing the "removed" filter under level-triggered mode,
   and `OnWritable()` runs on an empty writing buffer, which the
   connection layer mistakes for "remote closed" and kills the
   connection. EV_DISABLE (not EV_DELETE) is what we want -- it
   pauses delivery while preserving the filter's internal readiness
   state, mirroring epoll's behaviour. (EV_DELETE + later EV_ADD on
   a socket that is already readable produces no edge with EV_CLEAR,
   and the reader stalls forever.)

2. `Wait` no longer fans an EV_EOF flag out into both kPollerRead
   and kPollerWrite. EV_EOF on EVFILT_READ just means the peer
   half-closed for reading; the write direction is still usable
   until the next write returns EPIPE. The old fan-out manufactured
   a fake write event whenever the read side EOF'd, kicking
   `OnWritable()` on an empty writing buffer -- same false-disconnect
   chain as #1.

* io/util/socket: SetCloseOnExec must use F_SETFD; rename helper to SetStatusFlags

FD_CLOEXEC is a *file-descriptor* flag (F_GETFD / F_SETFD), not a
file-status flag (F_GETFL / F_SETFL). The existing `SetCloseOnExec`
routed through the generic `SetFlags` helper, which goes through
F_SETFL, and so passed FD_CLOEXEC = 0x01 into the status-flag space.

On Linux glibc, the unsupported bit is silently discarded by F_SETFL,
so this was invisible. On Darwin the kernel actually interprets the
0x01 bit inside the F_SETFL flag space: O_NONBLOCK gets wiped off the
listening socket and O_APPEND gets set. The downstream effect is that
`accept(2)` on the listening socket reverts to blocking and the
server's accept loop hangs forever after the first connection.

Fix `SetCloseOnExec` to use F_GETFD / F_SETFD directly, and rename
the generic helper from `SetFlags` to `SetStatusFlags` so the
descriptor-flag-vs-status-flag distinction is hard to miss next time.
The only remaining caller of the helper is `SetNonBlocking`, which
is correct (O_NONBLOCK *is* a status flag).

* io/native: treat ENOTCONN as transient in FlushWritingBuffer

When a non-blocking TCP socket is in the middle of its `connect(2)`,
Linux returns `EAGAIN` on `write(2)`/`sendmsg(2)`, which flare maps
to `FlushStatus::SystemBufferSaturated` and retries via the write
event. Darwin instead returns `ENOTCONN` ("Socket is not connected")
in the same situation -- the kernel distinguishes "would block on a
connected socket" (EAGAIN) from "not yet connected" (ENOTCONN).

The existing code treated ENOTCONN as a fatal write error, returned
`NothingWritten`, and `Kill(CleanupReason::Disconnect)` immediately
fired. The first RPC on every channel died with STATUS_IO_ERROR.

Fold ENOTCONN into the EAGAIN / EWOULDBLOCK branch so the write is
deferred until the kernel signals writability (which happens when
the connect completes), matching Linux semantics.

* base/write_mostly: order members so mutex outlives TLS wrappers

`WriteMostly` had `tls_buffer_` declared before `mutex`, which means
`mutex` was destroyed *first* (members are destroyed in reverse
declaration order). When `tls_buffer_` was then torn down, each
per-thread `WriteBufferWrapper` ran its destructor and tried to
`std::unique_lock(parent_->mutex)` -- locking an already-destroyed
mutex.

libstdc++ on Linux silently allowed this (the underlying pthread
mutex didn't actually reject), so the bug was invisible.

libc++ on Darwin returns EINVAL from `pthread_mutex_lock`, std::mutex
throws `system_error("mutex lock failed: Invalid argument")`, and
since destructors are noexcept by default the unhandled exception
aborts via std::terminate.

Reorder the members so `tls_buffer_` is the last declared field
(thus the first destroyed): both `mutex` and `exited_thread_combined_`
are still alive while the wrappers merge into them.

* base/deferred_test: initialise bool to avoid UB read

`Deferred.All` declared `bool f4;` and then `ASSERT_FALSE(f4)`
before any store. Reading an uninitialised bool is undefined
behaviour: the value is "indeterminate" and a load of an
indeterminate value is itself UB regardless of which bit pattern
happens to be in the stack slot.

Linux happened to observe zero on the stack and slipped through.
macOS Clang traps on the indeterminate read (SIGTRAP, exit 133),
exposing the bug.

Initialise `bool f4 = false`.

* macOS port: platform-specific test adjustments

A bag of test-only changes that fall out of the macOS port.

  * attribute_test.cc -- SetCurrentAffinity skipped on Darwin.
    Darwin has no pthread_setaffinity_np equivalent and the legacy
    thread_policy_set THREAD_AFFINITY_POLICY is hint-only; the
    `expected == GetCurrentProcessorId()` assertion cannot hold.

  * descriptor_test.cc -- ConcurrentRestartRead skipped on Darwin.
    The test attaches a PipeDesc with Event::Write to the read end
    of a closed-write pipe. Linux epoll never fires EPOLLOUT on
    that fd; macOS kqueue fires EVFILT_WRITE (no write buffer ==
    "has space"). The fire trips a pre-existing race in
    `Descriptor::QueueCleanupCallbackCheck`: FireXxxEvent does not
    consult `cleanup_pending_` before fetch_add, so an event between
    cleanup-task queueing and execution makes the strict CHECK at
    descriptor.cc abort. Fixing that race needs Dekker-style
    ordering between FireXxxEvent and Kill -- bigger scope than
    this batch.

  * writing_datagram_list_test.cc -- shrink the test datagram from
    16384 to 4096 bytes. Darwin's `net.inet.udp.maxdgram` defaults
    to 9216, and sendmsg() rejects larger UDP payloads with
    EMSGSIZE; Linux is happy up to ~64KB.

  * datagram_transceiver_test.cc -- cap the fragment count from
    60'000 to 8192 on Darwin. Still well above IOV_MAX (1024 on
    both libc's) so the "flatten on iovec overflow" path is still
    exercised; total payload now fits under net.inet.udp.maxdgram.

  * uri_matcher_test.cc -- regex pattern `"\\abc.*efg"` used the
    `\a` escape, which is not a valid ECMAScript escape. libstdc++
    accepted it silently (treating it as literal `a`); libc++ on
    Darwin throws regex_error("invalid escaped character") and the
    test aborts. The intended pattern was `abc.*efg`.

  * dirty_hook_test.cc, hooking_mock_test.cc -- skipped on Darwin.
    Both rely on `mprotect`-ing the text segment to writable to
    overwrite a function prologue. macOS arm64 enforces W^X on
    signed code pages: mprotect returns EPERM, the CHECK aborts.
    Toggling JIT-style write-execute would require
    `pthread_jit_write_protect_np`, the
    `com.apple.security.cs.allow-jit` entitlement, and code placed
    in a JIT region -- none of which apply to the regular text
    segment that DirtyHook patches. (The single non-hook subtest
    in hooking_mock_test, `NotEnabled`, still runs.)

* fixup: satisfy clang-format-15 (LLVM style) on the macOS port commits

CI's clang-format-15 (LLVM style) flagged two spots from the macOS
port batch:

  * flare/fiber/detail/context.h: `} // extern "C"` -> `}  // extern "C"`
    (LLVM trailing-comment style wants two spaces before //).

  * flare/io/detail/kqueue_poller.cc: the `read_flags`/`write_flags`
    ternaries were hand-aligned to a column width that clang-format
    didn't pick. Drop the explicit alignment and pull the disabled
    case into a named `disable_flags` local so each assignment fits
    on a single line that clang-format leaves alone.

No behaviour change.

* thirdparty/blade: bump blade.zip for target_arch cache fix

Picks up the toolchain.target_arch caching from
https://github.com/blade-build/blade-build/pull/1224.
BUILD-file load on this workspace drops from 13.2s to 0.25s.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 17:36:21 +08:00
Luo Bo f92cb6132e Always use 'lib' as libdir for cmake targets. The default one is not consistent across Linux distros. (#7) 2021-05-29 03:56:21 +08:00
Luo Bo 9cc00d3105 chore: Importing from internal repository. 2021-05-26 14:11:38 +08:00