vendor tsgo
This commit is contained in:
553
tools/tsgo/internal/fswatch/fsevents_darwin_ffi.go
Normal file
553
tools/tsgo/internal/fswatch/fsevents_darwin_ffi.go
Normal file
@@ -0,0 +1,553 @@
|
||||
//go:build darwin && (amd64 || arm64)
|
||||
|
||||
package fswatch
|
||||
|
||||
import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// fsevents_darwin_ffi.go: cgo-free macOS CoreFoundation / CoreServices FFI
|
||||
//
|
||||
// Provides Go access to Apple's FSEvents, CoreFoundation, and libdispatch
|
||||
// frameworks entirely without cgo, following the pattern established by
|
||||
// crypto/x509/internal/macos in the Go standard library.
|
||||
//
|
||||
// Imported symbols include CoreFoundation helpers (CFRelease,
|
||||
// CFStringCreateWithCString, CFArrayCreate), libdispatch
|
||||
// (dispatch_queue_create), and CoreServices FSEvents functions
|
||||
// (FSEventStreamCreate, SetDispatchQueue, Start, Stop, Invalidate,
|
||||
// Release).
|
||||
//
|
||||
// Each framework symbol has three parts:
|
||||
// 1. //go:cgo_import_dynamic: tells the linker to import the C symbol
|
||||
// from a shared library (CoreFoundation.framework, CoreServices.framework,
|
||||
// or libSystem.B.dylib).
|
||||
// 2. A TEXT trampoline in the .s file: a minimal assembly stub that JMPs
|
||||
// to the imported symbol. For simple functions this is a bare JMP; for
|
||||
// FSEventStreamCreate the trampoline also moves the float64 latency
|
||||
// argument from an integer register to a float register.
|
||||
// 3. A GLOBL/DATA pair that exports the trampoline's ABI0 address as a Go
|
||||
// uintptr variable (·fse_X_trampoline_addr), which the Go wrapper
|
||||
// passes to runtime's syscall_syscall6.
|
||||
//
|
||||
// ┌──────────────────────────────────────────────────────────┐
|
||||
// │ Go wrapper: cfRelease(ref) │
|
||||
// │ syscall_syscall6(trampoline_addr, ref, ...) │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌──────────────────────────────────┐ │
|
||||
// │ │ .s trampoline (ABI0) │ │
|
||||
// │ │ fse_CFRelease_trampoline<>: │ │
|
||||
// │ │ JMP fse_CFRelease(SB) │ │
|
||||
// │ └─────────────┬────────────────────┘ │
|
||||
// │ │ │
|
||||
// │ ▼ │
|
||||
// │ ┌──────────────────────────────────┐ │
|
||||
// │ │ //go:cgo_import_dynamic │ │
|
||||
// │ │ CFRelease from CoreFoundation │ │
|
||||
// │ └──────────────────────────────────┘ │
|
||||
// └──────────────────────────────────────────────────────────┘
|
||||
//
|
||||
// FSEvents callback synchronization (per-stream):
|
||||
//
|
||||
// GCD dispatch queue thread Go goroutine (eventLoop)
|
||||
// ───────────────────────── ────────────────────────
|
||||
// FSEvents fires C callback
|
||||
// on a libdispatch OS thread
|
||||
// │
|
||||
// ┌──────▼──────────────────┐
|
||||
// │ asm: retain CFArray │
|
||||
// │ paths, copy flags, │
|
||||
// │ allocate payload │
|
||||
// └──────┬──────────────────┘
|
||||
// │
|
||||
// write(eventPipeWrite, payload*) ─► read(eventFile) unblocks
|
||||
// │ │
|
||||
// asm: return to FSEvents fsEventsCallback(cb, payload)
|
||||
// classifies events,
|
||||
// frees payload,
|
||||
// routes to matching dirWatch events
|
||||
//
|
||||
// The assembly callback never enters Go ABI; it stays entirely in C
|
||||
// context. One pipe per stream hands retained/copied callback payloads from
|
||||
// the C dispatch queue thread to a dedicated Go event-loop goroutine.
|
||||
// The Go side uses os.File.Read (integrated with netpoll/kqueue on macOS)
|
||||
// so the goroutine parks efficiently without blocking an OS thread.
|
||||
//
|
||||
// streamCallback memory layout (must match assembly offsets):
|
||||
//
|
||||
// offset 0: eventPipeWrite fd Read by asm to call write()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Framework linker flags for the external linker.
|
||||
// Note: //go:cgo_ldflag is only valid in cgo-generated code. The
|
||||
// //go:cgo_import_dynamic directives below are sufficient: the Go
|
||||
// linker records the framework paths in the Mach-O LC_LOAD_DYLIB
|
||||
// commands automatically.
|
||||
|
||||
// Implemented in the runtime package (runtime/sys_darwin.go).
|
||||
// These are the same linknames that golang.org/x/sys/unix uses.
|
||||
//
|
||||
//go:linkname syscall_syscall6 syscall.syscall6
|
||||
func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CoreFoundation imports, trampoline addresses, and Go wrappers.
|
||||
//
|
||||
// Each function groups its //go:cgo_import_dynamic directive, its
|
||||
// trampoline address variable (populated by GLOBL/DATA in the .s files),
|
||||
// and its Go wrapper together.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
//go:cgo_import_dynamic fse_CFRelease CFRelease "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFRelease_trampoline_addr uintptr
|
||||
|
||||
func cfRelease(ref uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_CFRelease_trampoline_addr, ref, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_CFStringCreateWithCString CFStringCreateWithCString "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFStringCreateWithCString_trampoline_addr uintptr
|
||||
|
||||
func cfStringCreate(allocator uintptr, cstr unsafe.Pointer, encoding uint32) uintptr {
|
||||
ret, _, _ := syscall_syscall6(fse_CFStringCreateWithCString_trampoline_addr, allocator, uintptr(cstr), uintptr(encoding), 0, 0, 0)
|
||||
runtime.KeepAlive(cstr)
|
||||
return ret
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_CFArrayCreate CFArrayCreate "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFArrayCreate_trampoline_addr uintptr
|
||||
|
||||
func cfArrayCreate(allocator uintptr, values unsafe.Pointer, count int, callbacks uintptr) uintptr {
|
||||
ret, _, _ := syscall_syscall6(fse_CFArrayCreate_trampoline_addr, allocator, uintptr(values), uintptr(count), callbacks, 0, 0)
|
||||
runtime.KeepAlive(values)
|
||||
return ret
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_CFArrayGetValueAtIndex CFArrayGetValueAtIndex "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFArrayGetValueAtIndex_trampoline_addr uintptr
|
||||
|
||||
func cfArrayGetValueAtIndex(array uintptr, index int) uintptr {
|
||||
ret, _, _ := syscall_syscall6(fse_CFArrayGetValueAtIndex_trampoline_addr, array, uintptr(index), 0, 0, 0, 0)
|
||||
return ret
|
||||
}
|
||||
|
||||
// ----- NFC normalization helpers -----
|
||||
//
|
||||
// FSEvents reports paths using whatever bytes are stored on disk. APFS is
|
||||
// normalization-insensitive for lookups (a file created as NFD opens fine
|
||||
// under the NFC form, and vice versa) but it stores and reports the original
|
||||
// bytes. The library normalizes every path that crosses the darwin boundary
|
||||
// to Unicode NFC so that:
|
||||
// - WatchDirectory("/.../caf\u00e9") and WatchDirectory("/.../cafe\u0301")
|
||||
// coalesce to a single dir watch;
|
||||
// - WatchFile filters by exact-string compare in NFC always match;
|
||||
// - subscribers can compare event paths against their own NFC strings.
|
||||
//
|
||||
// All-ASCII inputs are bit-identical in NFC and NFD, so the hot path skips
|
||||
// the FFI entirely. The rare non-ASCII case round-trips through CoreFoundation
|
||||
// (UTF-8 → CFString → CFMutableString → CFStringNormalize → UTF-8) with no Go
|
||||
// Unicode tables, no extra dependency.
|
||||
|
||||
const (
|
||||
cfStringNormalizationFormC = 2 // kCFStringNormalizationFormC
|
||||
)
|
||||
|
||||
//go:cgo_import_dynamic fse_CFStringCreateMutableCopy CFStringCreateMutableCopy "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFStringCreateMutableCopy_trampoline_addr uintptr
|
||||
|
||||
func cfStringCreateMutableCopy(allocator uintptr, maxLength int, str uintptr) uintptr {
|
||||
ret, _, _ := syscall_syscall6(fse_CFStringCreateMutableCopy_trampoline_addr, allocator, uintptr(maxLength), str, 0, 0, 0)
|
||||
return ret
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_CFStringNormalize CFStringNormalize "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFStringNormalize_trampoline_addr uintptr
|
||||
|
||||
func cfStringNormalize(mutStr uintptr, form uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_CFStringNormalize_trampoline_addr, mutStr, form, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_CFStringGetLength CFStringGetLength "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFStringGetLength_trampoline_addr uintptr
|
||||
|
||||
func cfStringGetLength(str uintptr) int {
|
||||
ret, _, _ := syscall_syscall6(fse_CFStringGetLength_trampoline_addr, str, 0, 0, 0, 0, 0)
|
||||
return int(ret)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_CFStringGetMaximumSizeForEncoding CFStringGetMaximumSizeForEncoding "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFStringGetMaximumSizeForEncoding_trampoline_addr uintptr
|
||||
|
||||
func cfStringGetMaximumSizeForEncoding(length int, encoding uint32) int {
|
||||
ret, _, _ := syscall_syscall6(fse_CFStringGetMaximumSizeForEncoding_trampoline_addr, uintptr(length), uintptr(encoding), 0, 0, 0, 0)
|
||||
return int(ret)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_CFStringGetCString CFStringGetCString "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
|
||||
var fse_CFStringGetCString_trampoline_addr uintptr
|
||||
|
||||
func cfStringGetCString(str uintptr, buf unsafe.Pointer, bufSize int, encoding uint32) bool {
|
||||
ret, _, _ := syscall_syscall6(fse_CFStringGetCString_trampoline_addr, str, uintptr(buf), uintptr(bufSize), uintptr(encoding), 0, 0)
|
||||
runtime.KeepAlive(buf)
|
||||
return ret != 0
|
||||
}
|
||||
|
||||
// isASCII reports whether every byte in s is below 0x80. Pure-ASCII paths
|
||||
// are identical in every Unicode normalization form, so we can skip the
|
||||
// CoreFoundation round-trip entirely, which is the overwhelming common case.
|
||||
func isASCII(s string) bool {
|
||||
for i := range len(s) {
|
||||
if s[i] >= 0x80 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// cfStringToNFC returns the CFString at src as a NFC-normalized Go string.
|
||||
// If normalization fails, it falls back to the unnormalized UTF-8 contents.
|
||||
// Returns "" only if both the normalized and unnormalized conversions fail
|
||||
// (e.g. src is not a CFString, or allocation fails).
|
||||
func cfStringToNFC(src uintptr) string {
|
||||
if src == 0 {
|
||||
return ""
|
||||
}
|
||||
if s := cfStringNormalizedToGo(src); s != "" {
|
||||
return s
|
||||
}
|
||||
return cfStringToGo(src)
|
||||
}
|
||||
|
||||
// cfStringNormalizedToGo returns the CFString at src as a NFC-normalized Go
|
||||
// string, or "" on any failure.
|
||||
func cfStringNormalizedToGo(src uintptr) string {
|
||||
mut := cfStringCreateMutableCopy(0, 0, src)
|
||||
if mut == 0 {
|
||||
return ""
|
||||
}
|
||||
defer cfRelease(mut)
|
||||
|
||||
cfStringNormalize(mut, cfStringNormalizationFormC)
|
||||
return cfStringToGo(mut)
|
||||
}
|
||||
|
||||
// cfStringToGo extracts the UTF-8 contents of the CFString at src as a Go
|
||||
// string, or "" on failure.
|
||||
func cfStringToGo(src uintptr) string {
|
||||
length := cfStringGetLength(src)
|
||||
bufSize := cfStringGetMaximumSizeForEncoding(length, cfStringEncodingUTF8) + 1
|
||||
buf := make([]byte, bufSize)
|
||||
if !cfStringGetCString(src, unsafe.Pointer(&buf[0]), bufSize, cfStringEncodingUTF8) {
|
||||
return ""
|
||||
}
|
||||
// CFStringGetCString writes a NUL terminator; trim it.
|
||||
n := 0
|
||||
for n < len(buf) && buf[n] != 0 {
|
||||
n++
|
||||
}
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
// normalizeNFC returns s in Unicode NFC (canonical composed) form. ASCII
|
||||
// inputs are returned unchanged. Non-ASCII inputs go through CoreFoundation;
|
||||
// if any step fails (e.g. invalid UTF-8 from a corrupt path), the original
|
||||
// string is returned so the caller still sees *something* rather than nothing.
|
||||
func normalizeNFC(s string) string {
|
||||
if isASCII(s) {
|
||||
return s
|
||||
}
|
||||
|
||||
cstr := append([]byte(s), 0)
|
||||
src := cfStringCreate(0, unsafe.Pointer(&cstr[0]), cfStringEncodingUTF8)
|
||||
runtime.KeepAlive(cstr)
|
||||
if src == 0 {
|
||||
return s
|
||||
}
|
||||
defer cfRelease(src)
|
||||
|
||||
normalized := cfStringToNFC(src)
|
||||
if normalized == "" {
|
||||
return s
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// libdispatch imports.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
//go:cgo_import_dynamic fse_dispatch_queue_create dispatch_queue_create "/usr/lib/libSystem.B.dylib"
|
||||
|
||||
var fse_dispatch_queue_create_trampoline_addr uintptr
|
||||
|
||||
func dispatchQueueCreate(label unsafe.Pointer) uintptr {
|
||||
ret, _, _ := syscall_syscall6(fse_dispatch_queue_create_trampoline_addr, uintptr(label), 0, 0, 0, 0, 0)
|
||||
runtime.KeepAlive(label)
|
||||
return ret
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_dispatch_release dispatch_release "/usr/lib/libSystem.B.dylib"
|
||||
|
||||
var fse_dispatch_release_trampoline_addr uintptr
|
||||
|
||||
func dispatchRelease(obj uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_dispatch_release_trampoline_addr, obj, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_dispatch_sync_f dispatch_sync_f "/usr/lib/libSystem.B.dylib"
|
||||
|
||||
var (
|
||||
fse_dispatch_sync_f_trampoline_addr uintptr
|
||||
fse_dispatch_noop_addr uintptr
|
||||
)
|
||||
|
||||
func dispatchSync(queue, context, work uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_dispatch_sync_f_trampoline_addr, queue, context, work, 0, 0, 0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CoreServices / FSEvents imports, trampoline addresses, and Go wrappers.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventStreamCreate FSEventStreamCreate "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventStreamCreate_trampoline_addr uintptr // arch-specific trampoline
|
||||
|
||||
func fsEventStreamCreate(allocator, callback uintptr, ctx unsafe.Pointer, paths uintptr, since uint64, latency float64) uintptr {
|
||||
// syscall_syscall6 only carries 6 integer args. The arch-specific
|
||||
// trampoline moves the latency bits from an integer register to the
|
||||
// float register and hardcodes flags =
|
||||
// kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents (0x11).
|
||||
ret, _, _ := syscall_syscall6(
|
||||
fse_FSEventStreamCreate_trampoline_addr,
|
||||
allocator,
|
||||
callback,
|
||||
uintptr(ctx),
|
||||
paths,
|
||||
uintptr(since),
|
||||
uintptr(math.Float64bits(latency)),
|
||||
)
|
||||
runtime.KeepAlive(ctx)
|
||||
return ret
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventStreamSetDispatchQueue FSEventStreamSetDispatchQueue "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventStreamSetDispatchQueue_trampoline_addr uintptr
|
||||
|
||||
func fsEventStreamSetDispatchQueue(stream, queue uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_FSEventStreamSetDispatchQueue_trampoline_addr, stream, queue, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventStreamStart FSEventStreamStart "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventStreamStart_trampoline_addr uintptr
|
||||
|
||||
func fsEventStreamStart(stream uintptr) uint8 {
|
||||
r1, _, _ := syscall_syscall6(fse_FSEventStreamStart_trampoline_addr, stream, 0, 0, 0, 0, 0)
|
||||
return uint8(r1)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventStreamFlushSync FSEventStreamFlushSync "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventStreamFlushSync_trampoline_addr uintptr
|
||||
|
||||
func fsEventStreamFlushSync(stream uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_FSEventStreamFlushSync_trampoline_addr, stream, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventsGetCurrentEventId FSEventsGetCurrentEventId "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventsGetCurrentEventId_trampoline_addr uintptr
|
||||
|
||||
func fsEventsGetCurrentEventID() uint64 {
|
||||
r1, _, _ := syscall_syscall6(fse_FSEventsGetCurrentEventId_trampoline_addr, 0, 0, 0, 0, 0, 0)
|
||||
return uint64(r1)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventStreamStop FSEventStreamStop "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventStreamStop_trampoline_addr uintptr
|
||||
|
||||
func fsEventStreamStop(stream uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_FSEventStreamStop_trampoline_addr, stream, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventStreamInvalidate FSEventStreamInvalidate "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventStreamInvalidate_trampoline_addr uintptr
|
||||
|
||||
func fsEventStreamInvalidate(stream uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_FSEventStreamInvalidate_trampoline_addr, stream, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
//go:cgo_import_dynamic fse_FSEventStreamRelease FSEventStreamRelease "/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices"
|
||||
|
||||
var fse_FSEventStreamRelease_trampoline_addr uintptr
|
||||
|
||||
func fsEventStreamRelease(stream uintptr) {
|
||||
_, _, _ = syscall_syscall6(fse_FSEventStreamRelease_trampoline_addr, stream, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Direct callback assembly imports.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// These symbols are called directly by fsEventsCallbackASM and have no Go
|
||||
// wrappers.
|
||||
//go:cgo_import_dynamic fse_CFRetain CFRetain "/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation"
|
||||
//go:cgo_import_dynamic fse_write write "/usr/lib/libSystem.B.dylib"
|
||||
//go:cgo_import_dynamic fse___error __error "/usr/lib/libSystem.B.dylib"
|
||||
//go:cgo_import_dynamic fse_malloc malloc "/usr/lib/libSystem.B.dylib"
|
||||
//go:cgo_import_dynamic fse_memcpy memcpy "/usr/lib/libSystem.B.dylib"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// libSystem imports, trampoline addresses, and Go wrappers.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
//go:cgo_import_dynamic fse_free free "/usr/lib/libSystem.B.dylib"
|
||||
|
||||
var fse_free_trampoline_addr uintptr
|
||||
|
||||
func libcFree(ptr uintptr) {
|
||||
if ptr != 0 {
|
||||
_, _, _ = syscall_syscall6(fse_free_trampoline_addr, ptr, 0, 0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Callback address.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// fsEventsCallbackAsmAddr is the address of the arch-specific callback
|
||||
// function defined in fsevents_darwin_ffi_{amd64,arm64}.s.
|
||||
var fsEventsCallbackAsmAddr uintptr
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-stream callback infrastructure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// streamCallback is the per-stream buffer shared between the C callback
|
||||
// assembly and the Go event loop goroutine. The assembly receives a pointer
|
||||
// to this struct as the FSEventStreamContext.info parameter and uses offset
|
||||
// addressing to access the pipe fd.
|
||||
//
|
||||
// The struct layout must match the assembly (fsevents_darwin_ffi_{amd64,arm64}.s):
|
||||
//
|
||||
// offset 0: eventPipeWrite fd
|
||||
type streamCallback struct {
|
||||
eventPipeWrite uintptr
|
||||
|
||||
// Go-only fields (not accessed by assembly, offset doesn't matter).
|
||||
eventFile *os.File
|
||||
queue uintptr // per-stream serial dispatch queue
|
||||
done chan struct{}
|
||||
watches []fseventsWatchSnapshot
|
||||
}
|
||||
|
||||
type fsEventsCallbackPayload struct {
|
||||
numEvents uintptr
|
||||
paths uintptr
|
||||
flags uintptr
|
||||
ids uintptr
|
||||
}
|
||||
|
||||
func (p *fsEventsCallbackPayload) close() {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if p.paths != 0 {
|
||||
cfRelease(p.paths)
|
||||
}
|
||||
libcFree(p.flags)
|
||||
libcFree(p.ids)
|
||||
libcFree(uintptr(unsafe.Pointer(p)))
|
||||
}
|
||||
|
||||
// newStreamCallback allocates a pinned streamCallback with its own pipe and
|
||||
// per-stream serial dispatch queue, and starts a goroutine to process
|
||||
// callbacks. The per-stream serial queue serializes this stream's callbacks
|
||||
// and prevents cross-stream head-of-line blocking that a process-wide serial
|
||||
// queue would cause.
|
||||
func newStreamCallback(watches []fseventsWatchSnapshot) (*streamCallback, error) {
|
||||
var eventPipe [2]int
|
||||
if err := unix.Pipe(eventPipe[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unix.CloseOnExec(eventPipe[0])
|
||||
unix.CloseOnExec(eventPipe[1])
|
||||
|
||||
label := []byte("typescript.fswatch.fsevents.stream\x00")
|
||||
queue := dispatchQueueCreate(unsafe.Pointer(&label[0]))
|
||||
runtime.KeepAlive(label)
|
||||
if queue == 0 {
|
||||
unix.Close(eventPipe[0])
|
||||
unix.Close(eventPipe[1])
|
||||
return nil, errStreamCreateNull
|
||||
}
|
||||
|
||||
cb := &streamCallback{
|
||||
eventPipeWrite: uintptr(eventPipe[1]),
|
||||
eventFile: os.NewFile(uintptr(eventPipe[0]), "fsevents-event"),
|
||||
queue: queue,
|
||||
done: make(chan struct{}),
|
||||
watches: slices.Clone(watches),
|
||||
}
|
||||
go cb.eventLoop()
|
||||
return cb, nil
|
||||
}
|
||||
|
||||
func (cb *streamCallback) waitDispatchQueue() {
|
||||
if cb.queue != 0 {
|
||||
dispatchSync(cb.queue, 0, fse_dispatch_noop_addr)
|
||||
}
|
||||
}
|
||||
|
||||
// close shuts down the event loop goroutine and releases resources.
|
||||
func (cb *streamCallback) close() {
|
||||
unix.Close(int(cb.eventPipeWrite))
|
||||
<-cb.done
|
||||
cb.eventFile.Close()
|
||||
if cb.queue != 0 {
|
||||
dispatchRelease(cb.queue)
|
||||
cb.queue = 0
|
||||
}
|
||||
}
|
||||
|
||||
// eventLoop runs on a dedicated goroutine for this stream. It reads signals
|
||||
// from the callback assembly (via eventPipe) and processes each retained/copied
|
||||
// payload.
|
||||
// The eventFile.Read() call integrates with Go's netpoll (kqueue on macOS),
|
||||
// so the goroutine parks without blocking an OS thread while idle.
|
||||
func (cb *streamCallback) eventLoop() {
|
||||
defer close(cb.done)
|
||||
var payload *fsEventsCallbackPayload
|
||||
buf := unsafe.Slice((*byte)(unsafe.Pointer(&payload)), unsafe.Sizeof(payload))
|
||||
for {
|
||||
payload = nil
|
||||
if _, err := io.ReadFull(cb.eventFile, buf); err != nil {
|
||||
return // pipe closed or error → shutdown
|
||||
}
|
||||
|
||||
fsEventsCallback(cb, payload)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user