You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Rust编写Node扩展监听Mac时区变更且不阻塞主线程?

非阻塞监听Mac系统时区变更的Node.js Rust扩展实现

问题背景

我想用Rust结合core_foundation_sys和neon库实现一个Node.js扩展,暴露addListener API,当Mac系统时区修改时触发回调。最初的代码能成功收到时区变更通知,但会阻塞Node.js主线程;尝试将监听逻辑移到子线程后,又无法收到任何通知。

初始阻塞代码

extern "C" fn notification_callback(
    _center: notification_center::CFNotificationCenterRef,
    _observer: *mut c_void,
    _name: notification_center::CFNotificationName,
    _object: *const c_void,
    _user_info: CFDictionaryRef,
) {
    println!("timezone_change");
}

fn init_notification() {
  unsafe {
            let center = notification_center::CFNotificationCenterGetLocalCenter();
            let observer = &Observer as *const _ as *mut c_void;
            let name = CFStringCreateWithCString(
                kCFAllocatorDefault,
                b"kCFTimeZoneSystemTimeZoneDidChangeNotification\0".as_ptr() as *const i8,
                kCFStringEncodingUTF8,
            );
            let object = ptr::null();
            let suspension_behavior: isize =
                CFNotificationSuspensionBehaviorDeliverImmediately as isize;
            notification_center::CFNotificationCenterAddObserver(
                center,
                observer,
                notification_callback,
                name,
                object,
                suspension_behavior,
            );

            // 手动启动Run Loop导致主线程阻塞
            CFRunLoopRun();
    }
}

fn add_date_listener(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let cb_name_js = &cx.argument::<JsString>(0)?;
    let cb_name = cb_name_js.value(&mut cx);

    if cb_name == "timezone_change" {
        init_notification();
        TimeZoneChangeEvent::new(cx)
    } else {
        Ok(cx.undefined())
    }
}

// 模块初始化代码省略...

子线程尝试(无效)

fn init_notification() {
    thread::spawn(move || {
        unsafe {
            let center = notification_center::CFNotificationCenterGetLocalCenter();
            let observer = &Observer as *const _ as *mut c_void;
            let name = CFStringCreateWithCString(
                kCFAllocatorDefault,
                b"kCFTimeZoneSystemTimeZoneDidChangeNotification\0".as_ptr() as *const i8,
                kCFStringEncodingUTF8,
            );
            let object = ptr::null();
            let suspension_behavior: isize =
                CFNotificationSuspensionBehaviorDeliverImmediately as isize;
            notification_center::CFNotificationCenterAddObserver(
                center,
                observer,
                notification_callback,
                name,
                object,
                suspension_behavior,
            );

            CFRunLoopRun();
        }
    });
}

Node.js使用示例

const addon = require('dist/x.node');
addon.date.addListener('timezone_change', () => {
  console.log('timezone_change');
})

解决方案

核心问题分析

  1. 主线程阻塞原因:手动调用CFRunLoopRun()会让主线程进入无限循环,而Node.js在Mac平台本身已经基于CFRunLoop维护自己的事件循环,重复调用会阻塞Node.js的正常执行。
  2. 子线程无效原因:CFNotificationCenterGetLocalCenter()返回的是当前线程专属的本地通知中心,系统时区变更通知只会发送到主线程的通知中心,子线程的观察者无法接收;且子线程的Run Loop未与系统通知源正确绑定。

修正后的完整代码

use neon::prelude::*;
use std::sync::{Arc, Mutex};
use core_foundation_sys::{
    notification_center as nc,
    string::CFStringCreateWithCString,
    base::{kCFAllocatorDefault, kCFStringEncodingUTF8},
};
use lazy_static::lazy_static;

// 全局状态:保存JavaScript回调和跨线程通信Channel
struct ListenerState {
    callback: Option<Root<JsFunction>>,
    channel: Channel,
}

lazy_static! {
    static ref GLOBAL_STATE: Arc<Mutex<ListenerState>> = Arc::new(Mutex::new(ListenerState {
        callback: None,
        channel: Channel::new().unwrap(),
    }));
}

// Core Foundation通知回调
extern "C" fn timezone_notification_callback(
    _center: nc::CFNotificationCenterRef,
    _observer: *mut std::os::raw::c_void,
    _name: nc::CFNotificationName,
    _object: *const std::os::raw::c_void,
    _user_info: core_foundation_sys::dictionary::CFDictionaryRef,
) {
    let state = GLOBAL_STATE.lock().unwrap();
    // 通过Channel将事件传递到JavaScript线程
    state.channel.send(move |mut cx| {
        if let Some(callback) = &state.callback {
            let callback = callback.clone(&mut cx);
            // 调用JavaScript回调函数
            callback.call(&mut cx, cx.global(), vec![])?;
        }
        Ok(())
    }).unwrap();
}

fn init_timezone_listener() {
    unsafe {
        // 获取主线程的本地通知中心
        let center = nc::CFNotificationCenterGetLocalCenter();
        let observer = std::ptr::null_mut();
        
        // 创建时区变更通知的CFString名称
        let notification_name = CFStringCreateWithCString(
            kCFAllocatorDefault,
            b"kCFTimeZoneSystemTimeZoneDidChangeNotification\0".as_ptr() as *const i8,
            kCFStringEncodingUTF8,
        );
        
        // 添加通知观察者
        nc::CFNotificationCenterAddObserver(
            center,
            observer,
            Some(timezone_notification_callback),
            notification_name,
            std::ptr::null(),
            nc::CFNotificationSuspensionBehaviorDeliverImmediately as isize,
        );
        
        // 无需手动启动Run Loop,Node.js主线程已在运行
    }
}

// Node.js扩展的addListener API
fn add_timezone_listener(mut cx: FunctionContext) -> JsResult<JsUndefined> {
    let event_name = cx.argument::<JsString>(0)?.value(&mut cx);
    let callback = cx.argument::<JsFunction>(1)?;

    if event_name == "timezone_change" {
        let mut state = GLOBAL_STATE.lock().unwrap();
        // 保存JavaScript回调(Root防止被GC回收)
        state.callback = Some(callback.root(&mut cx));
        // 初始化监听(仅执行一次)
        init_timezone_listener();
    }

    Ok(cx.undefined())
}

// 模块初始化
fn create_date_module(cx: &mut ModuleContext) -> NeonResult<()> {
    let date = cx.empty_object();
    let add_listener_fn = JsFunction::new(cx, add_timezone_listener)?;
    date.set(cx, "addListener", add_listener_fn)?;
    cx.export_value("date", date)?;

    Ok(())
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    create_date_module(&mut cx)?;
    Ok(())
}

关键修改点

  1. 移除CFRunLoopRun():依赖Node.js主线程已有的CFRunLoop,避免阻塞主线程。
  2. 使用Neon的Channel:安全地将Core Foundation回调中的事件从Rust线程(主线程)传递到JavaScript线程,确保回调在Node.js的事件循环中执行。
  3. 全局状态管理:用Arc<Mutex>保存JavaScript回调的Root实例,防止回调被垃圾回收,同时保证多线程安全。
  4. 主线程添加观察者:在Node.js主线程调用init_timezone_listener(),确保观察者绑定到主线程的本地通知中心,能接收系统发送的时区变更通知。

内容的提问来源于stack exchange,提问作者刘北京

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 08:53:11