Skip to content

Conversation

@MistEO
Copy link
Member

@MistEO MistEO commented Nov 28, 2025

Summary by Sourcery

为 MaaFramework 新增一个实验性的 Rust 绑定,包括 FFI 层、高层 Rust API 和文档,并在集成文档中添加链接,说明这是一个由 AI 生成、尚未经过完整测试的接口。

新功能:

  • 引入一个 Rust binding crate,通过 C FFI 之上的类型安全、符合 Rust 习惯用法的 API 暴露 MaaFramework 的功能。
  • 为控制器、资源、任务器、上下文、作业、缓冲区、工具集助手以及通知提供 Rust 抽象,并支持自定义识别/动作。
  • 添加 Rust 使用示例,演示基础初始化、设备发现、控制器操作和资源加载。

文档:

  • 使用中英文为 Rust 绑定编写文档,并在集成指南中引用该文档,同时添加警告说明其为 AI 生成且处于实验阶段。
Original summary in English

Summary by Sourcery

Add an experimental Rust binding for MaaFramework, including FFI layer, high-level Rust API, and documentation, and link it from the integration docs as an AI-generated, not-yet-fully-tested interface.

New Features:

  • Introduce a Rust binding crate exposing MaaFramework functionality via a typed, idiomatic Rust API over the C FFI.
  • Provide Rust abstractions for controllers, resources, taskers, contexts, jobs, buffers, toolkit helpers, and notifications, plus support for custom recognition/actions.
  • Add example Rust usage demonstrating basic setup, device discovery, controller operations, and resource loading.

Documentation:

  • Document the Rust binding in both English and Chinese, and reference it from the integration guide with a warning that it is AI-generated and experimental.
Original summary in English

Summary by Sourcery

为 MaaFramework 新增一个实验性的 Rust 绑定,包括 FFI 层、高层 Rust API 和文档,并在集成文档中添加链接,说明这是一个由 AI 生成、尚未经过完整测试的接口。

新功能:

  • 引入一个 Rust binding crate,通过 C FFI 之上的类型安全、符合 Rust 习惯用法的 API 暴露 MaaFramework 的功能。
  • 为控制器、资源、任务器、上下文、作业、缓冲区、工具集助手以及通知提供 Rust 抽象,并支持自定义识别/动作。
  • 添加 Rust 使用示例,演示基础初始化、设备发现、控制器操作和资源加载。

文档:

  • 使用中英文为 Rust 绑定编写文档,并在集成指南中引用该文档,同时添加警告说明其为 AI 生成且处于实验阶段。
Original summary in English

Summary by Sourcery

Add an experimental Rust binding for MaaFramework, including FFI layer, high-level Rust API, and documentation, and link it from the integration docs as an AI-generated, not-yet-fully-tested interface.

New Features:

  • Introduce a Rust binding crate exposing MaaFramework functionality via a typed, idiomatic Rust API over the C FFI.
  • Provide Rust abstractions for controllers, resources, taskers, contexts, jobs, buffers, toolkit helpers, and notifications, plus support for custom recognition/actions.
  • Add example Rust usage demonstrating basic setup, device discovery, controller operations, and resource loading.

Documentation:

  • Document the Rust binding in both English and Chinese, and reference it from the integration guide with a warning that it is AI-generated and experimental.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Nov 28, 2025

Reviewer's Guide

添加一个实验性的、由 AI 生成的 MaaFramework Rust 绑定 crate,包括动态 FFI 层、安全的高级抽象(resource/controller/tasker/context/toolkit)、任务与通知模型、自定义动作/识别钩子、示例用法,以及更新文档以引用 Rust 集成并将其标记为实验性功能。

使用新绑定执行 Rust 任务的时序图

sequenceDiagram
    %% Flow: Rust app uses maa-framework crate to run a task on a device

    actor User
    participant App as RustApp
    participant Crate as MaaFrameworkCrate
    participant Toolkit as ToolkitModule
    participant Ctrl as AdbController
    participant Res as Resource
    participant Tasker as Tasker
    participant FFI as ffi_rs
    participant CApi as Maa_C_API

    User->>App: run()

    App->>Crate: load_library(path_to_framework)
    Crate->>FFI: load_symbol MaaVersion
    FFI->>CApi: dlopen + dlsym

    App->>Crate: load_toolkit(path_to_toolkit)
    Crate->>FFI: load_symbol MaaToolkitAdbDeviceListCreate
    FFI->>CApi: dlopen + dlsym

    App->>Toolkit: find_adb_devices()
    Toolkit->>FFI: maa_toolkit_adb_device_list_create
    FFI->>CApi: MaaToolkitAdbDeviceListCreate
    Toolkit->>FFI: maa_toolkit_adb_device_find
    FFI->>CApi: MaaToolkitAdbDeviceFind
    Toolkit-->>App: Vec<AdbDevice>

    App->>Ctrl: AdbController::new(device_info)
    Ctrl->>FFI: maa_adb_controller_create
    FFI->>CApi: MaaAdbControllerCreate
    Ctrl-->>App: AdbController

    App->>Res: Resource::new()
    Res->>FFI: maa_resource_create
    FFI->>CApi: MaaResourceCreate
    Res-->>App: Resource

    App->>Tasker: Tasker::new()
    Tasker->>FFI: maa_tasker_create
    FFI->>CApi: MaaTaskerCreate
    Tasker-->>App: Tasker

    App->>Tasker: bind(resource, controller)
    Tasker->>FFI: maa_tasker_bind_resource
    FFI->>CApi: MaaTaskerBindResource
    Tasker->>FFI: maa_tasker_bind_controller
    FFI->>CApi: MaaTaskerBindController

    App->>Tasker: post_task("StartUp", override_json)
    Tasker->>FFI: maa_tasker_post_task
    FFI->>CApi: MaaTaskerPostTask
    Tasker-->>App: TaskJob

    App->>Tasker: job.wait()
    Tasker->>FFI: maa_tasker_wait
    FFI->>CApi: MaaTaskerWait
    Tasker-->>App: Status::Succeeded/Failed

    App-->>User: print result and task detail
Loading

缓冲区、工具集、通知和自定义钩子的支持类图

classDiagram
    %% Supporting types: buffers, toolkit, notifications, custom hooks, errors

    class StringBuffer {
        - MaaStringBufferHandle handle
        - bool own
        + new() StringBuffer
        + from_handle(handle MaaStringBufferHandle) StringBuffer
        + handle() MaaStringBufferHandle
        + into_raw() MaaStringBufferHandle
        + get() Result~String~
        + set(value &str) Result~bool~
        + size() u64
    }

    class ImageBuffer {
        - MaaImageBufferHandle handle
        - bool own
        + new() ImageBuffer
        + from_handle(handle MaaImageBufferHandle) ImageBuffer
        + handle() MaaImageBufferHandle
        + get_raw_data() *mut u8
        + width() i32
        + height() i32
        + channels() i32
        + image_type() i32
        + set_raw_data(data &[u8], width i32, height i32, img_type i32) bool
        + to_vec() Vec~u8~
    }

    class RectBuffer {
        - MaaRectHandle handle
        + new() RectBuffer
        + handle() MaaRectHandle
        + get() Rect
        + set(rect &Rect) bool
    }

    class StringListBuffer {
        - MaaStringListBufferHandle handle
        + new() StringListBuffer
        + handle() MaaStringListBufferHandle
        + size() u64
        + get() Result~Vec~String~~
        + set(values &[&str]) Result~bool~
    }

    class ImageListBuffer {
        - MaaImageListBufferHandle handle
        + new() ImageListBuffer
        + handle() MaaImageListBufferHandle
        + size() u64
        + get() Vec~ImageBuffer~
    }

    class Rect {
        + i32 x
        + i32 y
        + i32 width
        + i32 height
        + new(x i32, y i32, width i32, height i32) Rect
    }

    class Toolkit {
        + init_option(user_path impl AsRef~Path~, default_config &Value) Result~bool~
        + find_adb_devices() Vec~AdbDevice~
        + find_adb_devices_specified(adb_path impl AsRef~Path~) Result~Vec~AdbDevice~~
        + find_desktop_windows() Vec~DesktopWindow~
    }

    class AdbDevice {
        + String name
        + PathBuf adb_path
        + String address
        + MaaAdbScreencapMethod screencap_methods
        + MaaAdbInputMethod input_methods
        + Value config
    }

    class DesktopWindow {
        + *mut c_void hwnd
        + String class_name
        + String window_name
    }

    class NotificationType {
        <<enum>>
        + Starting
        + Succeeded
        + Failed
        + Unknown
    }

    class ResourceLoadingDetail {
        + i64 res_id
        + String hash
        + String path
    }

    class ControllerActionDetail {
        + i64 ctrl_id
        + String uuid
        + String action
        + Value param
    }

    class TaskerTaskDetail {
        + i64 task_id
        + String entry
        + String uuid
        + String hash
    }

    class NextListItem {
        + String name
        + bool jump_back
        + bool anchor
    }

    class NodeNextListDetail {
        + i64 task_id
        + String name
        + Vec~NextListItem~ list
        + Value focus
    }

    class NodeRecognitionDetail {
        + i64 task_id
        + i64 reco_id
        + String name
        + Value focus
    }

    class NodeActionDetail {
        + i64 task_id
        + i64 action_id
        + String name
        + Value focus
    }

    class NodePipelineNodeDetail {
        + i64 task_id
        + i64 node_id
        + String name
        + Value focus
    }

    class ResourceEventSink {
        <<interface>>
        + on_resource_loading(noti_type NotificationType, detail ResourceLoadingDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class ControllerEventSink {
        <<interface>>
        + on_controller_action(noti_type NotificationType, detail ControllerActionDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class TaskerEventSink {
        <<interface>>
        + on_tasker_task(noti_type NotificationType, detail TaskerTaskDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class ContextEventSink {
        <<interface>>
        + on_node_next_list(noti_type NotificationType, detail NodeNextListDetail) void
        + on_node_recognition(noti_type NotificationType, detail NodeRecognitionDetail) void
        + on_node_action(noti_type NotificationType, detail NodeActionDetail) void
        + on_node_pipeline_node(noti_type NotificationType, detail NodePipelineNodeDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class CustomRecognitionArgs {
        + &Context context
        + MaaTaskId task_id
        + &str node_name
        + &str custom_recognition_name
        + &str custom_recognition_param
        + &ImageBuffer image
        + Rect roi
    }

    class CustomRecognitionResult {
        + bool hit
        + Rect box_
        + String detail
    }

    class CustomRecognition {
        <<interface>>
        + run(args CustomRecognitionArgs) Option~CustomRecognitionResult~
    }

    class CustomActionArgs {
        + &Context context
        + MaaTaskId task_id
        + &str node_name
        + &str custom_action_name
        + &str custom_action_param
        + MaaRecoId reco_id
        + Rect box_
    }

    class CustomAction {
        <<interface>>
        + run(args CustomActionArgs) bool
    }

    class FnRecognition~F~ {
        - F func
        + new(func F) FnRecognition~F~
        + run(args CustomRecognitionArgs) Option~CustomRecognitionResult~
    }

    class FnAction~F~ {
        - F func
        + new(func F) FnAction~F~
        + run(args CustomActionArgs) bool
    }

    class MaaError {
        <<enum>>
        + LibraryLoad
        + CreateFailed
        + OperationFailed
        + InvalidHandle
        + BufferError
        + JsonError
        + Utf8Error
        + NullPointer
        + TaskFailed
        + ConnectionFailed
        + ResourceLoadFailed
        + InvalidId
    }

    StringListBuffer --> StringBuffer : uses
    ImageListBuffer --> ImageBuffer : uses

    Toolkit --> AdbDevice : returns
    Toolkit --> DesktopWindow : returns

    ResourceEventSink <|.. ControllerEventSink
    ResourceEventSink <|.. TaskerEventSink
    ResourceEventSink <|.. ContextEventSink

    FnRecognition ..|> CustomRecognition
    FnAction ..|> CustomAction
Loading

文件级改动

Change Details Files
引入一个 Rust crate,通过 libloading 动态绑定 MaaFramework 和 MaaToolkit 的 C API,并以符合 Rust 习惯的模块形式对外暴露。
  • 在 Cargo.toml 中为 Rust 绑定添加 crate 元数据、依赖和特性(features)
  • 在 lib.rs 中使用 OnceLock 管理的 libloading::Library 句柄在运行时加载 MaaFramework 和 MaaToolkit 共享库
  • 在 ffi.rs 中将 FFI 接口定义为 C 符号的轻量封装,使用 load_symbol! 宏按需解析每个函数
source/binding/Rust/Cargo.toml
source/binding/Rust/src/lib.rs
source/binding/Rust/src/ffi.rs
提供强类型的 Rust 表达,用于描述 MaaFramework 的核心概念(id、句柄、选项、枚举、矩形),并包含便捷工具函数和基础错误处理。
  • 在 define.rs 中定义基础类型、句柄、状态与日志枚举、选项枚举、控制器模式 bitflags、Rect 结构体以及算法/动作枚举
  • 使用 thiserror 引入 MaaError 和 Result 类型,以统一该绑定中的错误报告
  • 将原始 C 缓冲区(字符串、图像、矩形、字符串列表、图像列表)封装为 RAII 管理的类型,负责分配、析构和安全访问模式
source/binding/Rust/src/define.rs
source/binding/Rust/src/error.rs
source/binding/Rust/src/buffer.rs
为资源、控制器、任务器和上下文暴露更高层的抽象,包括类 job 的异步句柄以及工具集工具函数。
  • 使用 Resource 结构体封装 MaaResource,管理资源包加载、pipeline/next/image 覆盖、推理设备配置以及元数据查询
  • 封装 MaaController,并细化为 AdbController、Win32Controller 和 DbgController,提供输入、应用控制、截屏及选项等高级方法
  • 封装 MaaTasker 和 MaaContext,用于协调任务、同步识别/动作执行、任务/节点/识别/动作详情检查以及缓存管理
  • 引入 TaskJob、CtrlJob 和 ResJob 类型,用于轮询和等待异步操作,外加一个通用 Job 辅助工具
  • 添加 Toolkit 辅助工具,用于发现 ADB 设备和桌面窗口、初始化工具集选项,并将 C 结构体映射为 Rust 结构体
source/binding/Rust/src/resource.rs
source/binding/Rust/src/controller.rs
source/binding/Rust/src/tasker.rs
source/binding/Rust/src/context.rs
source/binding/Rust/src/job.rs
source/binding/Rust/src/toolkit.rs
为自定义识别/动作和结构化通知添加可扩展性与事件管道。
  • 定义 CustomRecognition 和 CustomAction trait,以及函数适配器封装,使用户可以用纯 Rust 实现自定义行为
  • 添加 notification.rs,包含枚举、已解析的详情结构体、消息常量以及用于资源、控制器、任务器和节点级事件的解析辅助函数
  • 为资源、控制器、任务器和上下文引入轻量的事件接收(event-sink) trait,以便用户将通知路由到自己的处理器
source/binding/Rust/src/custom.rs
source/binding/Rust/src/notification.rs
为 Rust 绑定编写文档和示例,将其在框架级文档和 crate 级 README 与示例中标记为 AI 生成且实验性。
  • 在 Rust 绑定目录中添加英文 README 和中文 README.zh_cn,说明安装、快速开始、模块结构和 API 概览,并包含明确的 AI 生成/实验性警告
  • 提供一个基础示例程序,使用 Rust API 加载库、发现 ADB 设备、连接、截图,并可选地加载资源
  • 更新英文集成指南,在其中加入 Rust 章节,链接到绑定源码和示例,描述接口状态,并重申实验性/AI 生成的警告
source/binding/Rust/README.md
source/binding/Rust/README.zh_cn.md
source/binding/Rust/examples/basic.rs
docs/en_us/2.1-Integration.md

Tips and commands

Interacting with Sourcery

  • 触发新评审: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub issue: 在评审评论下回复,请求 Sourcery 从该评论创建一个 issue。你也可以直接回复评审评论 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成一个标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文的任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成评审者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成评审者指南。
  • 批量解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不希望再看到它们,这会很有用。
  • 批量撤销所有 Sourcery 评审: 在 pull request 中评论 @sourcery-ai dismiss,即可撤销所有现有的 Sourcery 评审。若你想从头开始新的评审,尤其有用——别忘了之后再评论 @sourcery-ai review 来触发新评审!

Customizing Your Experience

访问你的 dashboard 来:

  • 启用或禁用评审功能,例如 Sourcery 生成的 pull request 摘要、评审者指南等。
  • 更改评审语言。
  • 添加、移除或编辑自定义评审指令。
  • 调整其他评审设置。

Getting Help

Original review guide in English

Reviewer's Guide

Add an experimental, AI-generated Rust binding crate for MaaFramework, including a dynamic FFI layer, safe high-level abstractions (resource/controller/tasker/context/toolkit), job and notification models, custom action/recognition hooks, example usage, and documentation updates to reference the Rust integration and mark it as experimental.

Sequence diagram for a Rust task execution using the new binding

sequenceDiagram
    %% Flow: Rust app uses maa-framework crate to run a task on a device

    actor User
    participant App as RustApp
    participant Crate as MaaFrameworkCrate
    participant Toolkit as ToolkitModule
    participant Ctrl as AdbController
    participant Res as Resource
    participant Tasker as Tasker
    participant FFI as ffi_rs
    participant CApi as Maa_C_API

    User->>App: run()

    App->>Crate: load_library(path_to_framework)
    Crate->>FFI: load_symbol MaaVersion
    FFI->>CApi: dlopen + dlsym

    App->>Crate: load_toolkit(path_to_toolkit)
    Crate->>FFI: load_symbol MaaToolkitAdbDeviceListCreate
    FFI->>CApi: dlopen + dlsym

    App->>Toolkit: find_adb_devices()
    Toolkit->>FFI: maa_toolkit_adb_device_list_create
    FFI->>CApi: MaaToolkitAdbDeviceListCreate
    Toolkit->>FFI: maa_toolkit_adb_device_find
    FFI->>CApi: MaaToolkitAdbDeviceFind
    Toolkit-->>App: Vec<AdbDevice>

    App->>Ctrl: AdbController::new(device_info)
    Ctrl->>FFI: maa_adb_controller_create
    FFI->>CApi: MaaAdbControllerCreate
    Ctrl-->>App: AdbController

    App->>Res: Resource::new()
    Res->>FFI: maa_resource_create
    FFI->>CApi: MaaResourceCreate
    Res-->>App: Resource

    App->>Tasker: Tasker::new()
    Tasker->>FFI: maa_tasker_create
    FFI->>CApi: MaaTaskerCreate
    Tasker-->>App: Tasker

    App->>Tasker: bind(resource, controller)
    Tasker->>FFI: maa_tasker_bind_resource
    FFI->>CApi: MaaTaskerBindResource
    Tasker->>FFI: maa_tasker_bind_controller
    FFI->>CApi: MaaTaskerBindController

    App->>Tasker: post_task("StartUp", override_json)
    Tasker->>FFI: maa_tasker_post_task
    FFI->>CApi: MaaTaskerPostTask
    Tasker-->>App: TaskJob

    App->>Tasker: job.wait()
    Tasker->>FFI: maa_tasker_wait
    FFI->>CApi: MaaTaskerWait
    Tasker-->>App: Status::Succeeded/Failed

    App-->>User: print result and task detail
Loading

Supporting class diagram for buffers, toolkit, notifications, and custom hooks

classDiagram
    %% Supporting types: buffers, toolkit, notifications, custom hooks, errors

    class StringBuffer {
        - MaaStringBufferHandle handle
        - bool own
        + new() StringBuffer
        + from_handle(handle MaaStringBufferHandle) StringBuffer
        + handle() MaaStringBufferHandle
        + into_raw() MaaStringBufferHandle
        + get() Result~String~
        + set(value &str) Result~bool~
        + size() u64
    }

    class ImageBuffer {
        - MaaImageBufferHandle handle
        - bool own
        + new() ImageBuffer
        + from_handle(handle MaaImageBufferHandle) ImageBuffer
        + handle() MaaImageBufferHandle
        + get_raw_data() *mut u8
        + width() i32
        + height() i32
        + channels() i32
        + image_type() i32
        + set_raw_data(data &[u8], width i32, height i32, img_type i32) bool
        + to_vec() Vec~u8~
    }

    class RectBuffer {
        - MaaRectHandle handle
        + new() RectBuffer
        + handle() MaaRectHandle
        + get() Rect
        + set(rect &Rect) bool
    }

    class StringListBuffer {
        - MaaStringListBufferHandle handle
        + new() StringListBuffer
        + handle() MaaStringListBufferHandle
        + size() u64
        + get() Result~Vec~String~~
        + set(values &[&str]) Result~bool~
    }

    class ImageListBuffer {
        - MaaImageListBufferHandle handle
        + new() ImageListBuffer
        + handle() MaaImageListBufferHandle
        + size() u64
        + get() Vec~ImageBuffer~
    }

    class Rect {
        + i32 x
        + i32 y
        + i32 width
        + i32 height
        + new(x i32, y i32, width i32, height i32) Rect
    }

    class Toolkit {
        + init_option(user_path impl AsRef~Path~, default_config &Value) Result~bool~
        + find_adb_devices() Vec~AdbDevice~
        + find_adb_devices_specified(adb_path impl AsRef~Path~) Result~Vec~AdbDevice~~
        + find_desktop_windows() Vec~DesktopWindow~
    }

    class AdbDevice {
        + String name
        + PathBuf adb_path
        + String address
        + MaaAdbScreencapMethod screencap_methods
        + MaaAdbInputMethod input_methods
        + Value config
    }

    class DesktopWindow {
        + *mut c_void hwnd
        + String class_name
        + String window_name
    }

    class NotificationType {
        <<enum>>
        + Starting
        + Succeeded
        + Failed
        + Unknown
    }

    class ResourceLoadingDetail {
        + i64 res_id
        + String hash
        + String path
    }

    class ControllerActionDetail {
        + i64 ctrl_id
        + String uuid
        + String action
        + Value param
    }

    class TaskerTaskDetail {
        + i64 task_id
        + String entry
        + String uuid
        + String hash
    }

    class NextListItem {
        + String name
        + bool jump_back
        + bool anchor
    }

    class NodeNextListDetail {
        + i64 task_id
        + String name
        + Vec~NextListItem~ list
        + Value focus
    }

    class NodeRecognitionDetail {
        + i64 task_id
        + i64 reco_id
        + String name
        + Value focus
    }

    class NodeActionDetail {
        + i64 task_id
        + i64 action_id
        + String name
        + Value focus
    }

    class NodePipelineNodeDetail {
        + i64 task_id
        + i64 node_id
        + String name
        + Value focus
    }

    class ResourceEventSink {
        <<interface>>
        + on_resource_loading(noti_type NotificationType, detail ResourceLoadingDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class ControllerEventSink {
        <<interface>>
        + on_controller_action(noti_type NotificationType, detail ControllerActionDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class TaskerEventSink {
        <<interface>>
        + on_tasker_task(noti_type NotificationType, detail TaskerTaskDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class ContextEventSink {
        <<interface>>
        + on_node_next_list(noti_type NotificationType, detail NodeNextListDetail) void
        + on_node_recognition(noti_type NotificationType, detail NodeRecognitionDetail) void
        + on_node_action(noti_type NotificationType, detail NodeActionDetail) void
        + on_node_pipeline_node(noti_type NotificationType, detail NodePipelineNodeDetail) void
        + on_unknown_notification(msg &str, details &Value) void
    }

    class CustomRecognitionArgs {
        + &Context context
        + MaaTaskId task_id
        + &str node_name
        + &str custom_recognition_name
        + &str custom_recognition_param
        + &ImageBuffer image
        + Rect roi
    }

    class CustomRecognitionResult {
        + bool hit
        + Rect box_
        + String detail
    }

    class CustomRecognition {
        <<interface>>
        + run(args CustomRecognitionArgs) Option~CustomRecognitionResult~
    }

    class CustomActionArgs {
        + &Context context
        + MaaTaskId task_id
        + &str node_name
        + &str custom_action_name
        + &str custom_action_param
        + MaaRecoId reco_id
        + Rect box_
    }

    class CustomAction {
        <<interface>>
        + run(args CustomActionArgs) bool
    }

    class FnRecognition~F~ {
        - F func
        + new(func F) FnRecognition~F~
        + run(args CustomRecognitionArgs) Option~CustomRecognitionResult~
    }

    class FnAction~F~ {
        - F func
        + new(func F) FnAction~F~
        + run(args CustomActionArgs) bool
    }

    class MaaError {
        <<enum>>
        + LibraryLoad
        + CreateFailed
        + OperationFailed
        + InvalidHandle
        + BufferError
        + JsonError
        + Utf8Error
        + NullPointer
        + TaskFailed
        + ConnectionFailed
        + ResourceLoadFailed
        + InvalidId
    }

    StringListBuffer --> StringBuffer : uses
    ImageListBuffer --> ImageBuffer : uses

    Toolkit --> AdbDevice : returns
    Toolkit --> DesktopWindow : returns

    ResourceEventSink <|.. ControllerEventSink
    ResourceEventSink <|.. TaskerEventSink
    ResourceEventSink <|.. ContextEventSink

    FnRecognition ..|> CustomRecognition
    FnAction ..|> CustomAction
Loading

File-Level Changes

Change Details Files
Introduce a Rust crate that dynamically binds to the MaaFramework and MaaToolkit C APIs via libloading and exposes them as idiomatic Rust modules.
  • Add crate metadata, dependencies, and features for the Rust binding in Cargo.toml
  • Load MaaFramework and MaaToolkit shared libraries at runtime using OnceLock-managed libloading::Library handles in lib.rs
  • Define the FFI surface in ffi.rs as thin wrappers around C symbols, using a load_symbol! macro to resolve each function on-demand
source/binding/Rust/Cargo.toml
source/binding/Rust/src/lib.rs
source/binding/Rust/src/ffi.rs
Provide strongly-typed Rust representations of core MaaFramework concepts (ids, handles, options, enums, rects) with convenience helpers and basic error handling.
  • Define fundamental types, handles, status and logging enums, option enums, controller mode bitflags, Rect struct, and algorithm/action enums in define.rs
  • Introduce MaaError and Result types using thiserror to unify error reporting across the binding
  • Wrap raw C buffers (string, image, rect, string list, image list) into RAII-managed types that handle allocation, destruction, and safe access patterns
source/binding/Rust/src/define.rs
source/binding/Rust/src/error.rs
source/binding/Rust/src/buffer.rs
Expose higher-level abstractions for resources, controllers, taskers, and contexts, including job-style async handles and toolkit utilities.
  • Wrap MaaResource with a Resource struct that manages bundle loading, pipeline/next/image overrides, inference-device configuration, and metadata queries
  • Wrap MaaController and specialize into AdbController, Win32Controller, and DbgController with high-level methods for input, app control, screenshot, and options
  • Wrap MaaTasker and MaaContext to coordinate tasks, synchronous recognition/action runs, task/ node/recognition/action detail inspection, and cache management
  • Introduce TaskJob, CtrlJob, and ResJob types for polling and waiting on asynchronous operations, plus a generic Job helper
  • Add Toolkit helpers to discover ADB devices and desktop windows and to initialize toolkit options, mapping C structs to Rust structs
source/binding/Rust/src/resource.rs
source/binding/Rust/src/controller.rs
source/binding/Rust/src/tasker.rs
source/binding/Rust/src/context.rs
source/binding/Rust/src/job.rs
source/binding/Rust/src/toolkit.rs
Add extensibility and event plumbing for custom recognition/actions and structured notifications.
  • Define CustomRecognition and CustomAction traits plus function-adapter wrappers to allow users to implement custom behavior in pure Rust
  • Add notification.rs with enums, parsed detail structs, message constants, and parsing helpers for resource, controller, tasker, and node-level events
  • Introduce lightweight event-sink traits for resources, controllers, taskers, and contexts so users can route notifications to their own handlers
source/binding/Rust/src/custom.rs
source/binding/Rust/src/notification.rs
Document and demonstrate the Rust binding, marking it as AI-generated and experimental in both framework-level docs and crate-level READMEs and examples.
  • Add an English README and a Chinese README.zh_cn to the Rust binding directory, explaining installation, quick start, module layout, and API overview, with explicit AI-generated/experimental warnings
  • Provide a basic example program that loads the libraries, discovers an ADB device, connects, screenshots, and optionally loads resources using the Rust API
  • Update the English integration guide to include a Rust section that links to the binding source and examples, describes interface status, and reiterates the experimental/AI-generated warning
source/binding/Rust/README.md
source/binding/Rust/README.zh_cn.md
source/binding/Rust/examples/basic.rs
docs/en_us/2.1-Integration.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - 我已经审查了你的修改,并发现了一些需要处理的问题。

  • 有几条对外暴露的调用路径可能会触发 panic(load_symbol! 中使用 expect,在 resource.rstasker.rstoolkit.rs 以及示例代码中多处 CString::new(...).unwrap() 等)。建议从这些调用点返回 Result,或者把这些 panic 转换成 MaaError,这样在遇到不合法输入或缺失符号时,绑定层的行为更像一个库,而不是直接让整个进程崩溃。
  • TaskerControllerResourceDesktopWindow 这类类型在持有原始 FFI 句柄的情况下被标记为 unsafe impl Send/Sync;最好能在某个地方(比如注释)对这些并发安全保证进行说明,或者在 C API 没有明确声明线程安全时,在封装的调用外层加上合适的同步措施。
  • Resource 里的 custom_recognitionscustom_actions 字段目前完全没有被使用;如果后续计划支持自定义回调注册,建议现在就把这些字段接到底层 FFI 上;如果短期内不会支持,考虑先移除这些字段,以免造成困惑。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several public-facing paths can panic (`load_symbol!` using `expect`, multiple `CString::new(...).unwrap()` in `resource.rs`, `tasker.rs`, `toolkit.rs`, examples, etc.); consider returning `Result` from these call sites or converting the panics into `MaaError` so the binding behaves like a library rather than aborting the process on bad input or missing symbols.
- Types like `Tasker`, `Controller`, `Resource`, and `DesktopWindow` are marked `unsafe impl Send/Sync` while holding raw FFI handles; it would be good to either justify these guarantees (e.g. in comments) or wrap underlying calls with appropriate synchronization if the C API is not explicitly thread-safe.
- The `custom_recognitions` and `custom_actions` fields in `Resource` are currently never used; if registration of custom callbacks is planned, consider wiring these through to the underlying FFI now or removing the fields to avoid confusion until that support exists.

## Individual Comments

### Comment 1
<location> `source/binding/Rust/src/buffer.rs:206-210` </location>
<code_context>
+        Ok(result)
+    }
+
+    pub fn set(&self, values: &[&str]) -> Result<bool> {
+        for value in values {
+            let str_buf = StringBuffer::new();
+            str_buf.set(value)?;
+            if ffi::maa_string_list_buffer_append(self.handle, str_buf.handle()) == 0 {
+                return Ok(false);
+            }
</code_context>

<issue_to_address>
**issue (bug_risk):** StringListBuffer::set drops StringBuffers after appending, likely leaving the underlying list with dangling pointers.

Each `StringBuffer` is created with `own: true` and its handle appended to the list, then `str_buf` is dropped at end of the loop, calling `maa_string_buffer_destroy` while the list still stores the pointer. That’s a likely use-after-free on the C side.

You’ll need to change the ownership model here, e.g.:
- use an `into_raw` / `from_raw` pattern so ownership is transferred to the C list, or
- build the list fully on the C side so Rust never owns the child buffers.

At minimum, construct these via `StringBuffer::from_handle` with `own = false`, or otherwise ensure list elements are not dropped by Rust.
</issue_to_address>

### Comment 2
<location> `source/binding/Rust/src/resource.rs:103-104` </location>
<code_context>
+        self.set_inference(InferenceExecutionProvider::Cpu, InferenceDevice::Cpu)
+    }
+
+    pub fn use_directml(&self, device_id: i32) -> bool {
+        self.set_inference(InferenceExecutionProvider::DirectMl, unsafe {
+            std::mem::transmute(device_id)
+        })
</code_context>

<issue_to_address>
**issue (bug_risk):** Casting `i32` to `InferenceDevice` with `transmute` is unsafe and unnecessary here.

`use_directml` (and `use_coreml`) build `InferenceDevice` with `unsafe { std::mem::transmute(device_id) }`. This is UB for any `device_id` that doesn’t match a valid enum discriminant, and it isn’t needed since the C API ultimately wants an `i32`.

Prefer either:
- keeping `ep`/`device` as plain `i32` at this FFI boundary, or
- adding a safe `InferenceDevice::from_i32 -> Option<Self>` and handling invalid values.

This avoids `transmute` while keeping the API explicit and safe.
</issue_to_address>

### Comment 3
<location> `source/binding/Rust/src/tasker.rs:126-132` </location>
<code_context>
+        ffi::maa_tasker_inited(self.handle) != 0
+    }
+
+    pub fn post_task(&self, entry: &str, pipeline_override: &Value) -> Result<TaskJob> {
+        let entry_cstr = CString::new(entry).map_err(|e| MaaError::BufferError(e.to_string()))?;
+        let override_str = serde_json::to_string(pipeline_override)?;
+        let override_cstr =
+            CString::new(override_str).map_err(|e| MaaError::BufferError(e.to_string()))?;
+
+        let id =
+            ffi::maa_tasker_post_task(self.handle, entry_cstr.as_ptr(), override_cstr.as_ptr());
+        Ok(TaskJob::new(id, self.handle))
</code_context>

<issue_to_address>
**suggestion (bug_risk):** No handling for invalid task IDs (MAA_INVALID_ID) returned from FFI when creating TaskJob.

The C API uses `MAA_INVALID_ID == 0`, but `post_task` always constructs a `TaskJob` from whatever ID is returned. If the FFI call fails and returns `MAA_INVALID_ID`, subsequent `wait`/`status` calls may misbehave or even hit UB depending on the C side.

Consider checking the result and returning an error when it equals `MAA_INVALID_ID`, similar to your null-handle checks elsewhere, e.g.:
```rust
let id = ffi::maa_tasker_post_task(self.handle, entry_cstr.as_ptr(), override_cstr.as_ptr());
if id == MAA_INVALID_ID {
    return Err(MaaError::TaskFailed); // or a more specific error
}
Ok(TaskJob::new(id, self.handle))
```
You may also want to apply the same pattern to other FFI calls that return task/controller/resource IDs if they use `MAA_INVALID_ID` as well.

Suggested implementation:

```rust
    pub fn post_task(&self, entry: &str, pipeline_override: &Value) -> Result<TaskJob> {
        let entry_cstr = CString::new(entry).map_err(|e| MaaError::BufferError(e.to_string()))?;
        let override_str = serde_json::to_string(pipeline_override)?;
        let override_cstr =
            CString::new(override_str).map_err(|e| MaaError::BufferError(e.to_string()))?;

        let id =
            ffi::maa_tasker_post_task(self.handle, entry_cstr.as_ptr(), override_cstr.as_ptr());

        if id == ffi::MAA_INVALID_ID {
            // FFI reported a failure to create the task; surface this as a Rust error
            return Err(MaaError::TaskFailed("failed to post task".to_string()));
        }

        Ok(TaskJob::new(id, self.handle))
    }

```

1. Ensure that the FFI constant `MAA_INVALID_ID` is exposed under `ffi::MAA_INVALID_ID`.  
   - If it is defined elsewhere (e.g., as a top-level constant), adjust the comparison accordingly (for example, `if id == MAA_INVALID_ID`).

2. Confirm that `MaaError::TaskFailed(String)` (or the constructor used here) exists in your error type definition.  
   - If it does not, add an appropriate variant to your `MaaError` enum (e.g. `TaskFailed(String)` or a more general FFI error) and update its `Display`/`Error` implementations as needed.  
   - If your project uses a different error style (e.g. `MaaError::TaskFailed` without payload, or `MaaError::TaskError { reason: String }`), adjust the `return Err(...)` line to match.

3. Consider applying the same `MAA_INVALID_ID` check to other FFI functions that return IDs (e.g. `maa_tasker_post_stop`) and, if appropriate, changing their return type from `TaskJob` to `Result<TaskJob>` to propagate errors instead of constructing a `TaskJob` for an invalid ID.
</issue_to_address>

### Comment 4
<location> `source/binding/Rust/src/tasker.rs:84-89` </location>
<code_context>
+        self.handle
+    }
+
+    pub fn bind(&mut self, resource: Resource, controller: Controller) -> bool {
+        let res_ret = ffi::maa_tasker_bind_resource(self.handle, resource.handle());
+        let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
+        self._resource_holder = Some(resource);
+        self._controller_holder = Some(controller);
+        res_ret != 0 && ctrl_ret != 0
+    }
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** bind/bind_resource/bind_controller store holders even when the underlying bind call fails.

Here the `Resource` and `Controller` are stored in `_resource_holder` / `_controller_holder` even if the FFI bind calls fail. That means callers can see `false` but the `Tasker` still holds the handles, which may be surprising and could mask failed bindings.

Either only store the holders when both FFI calls succeed, or keep the current behavior but document that `Tasker` may retain resources/controllers purely for lifetime management after a failed bind, and consider exposing an explicit "is bound" check (e.g., via `maa_tasker_inited`).

Suggested implementation:

```rust
    pub fn bind(&mut self, resource: Resource, controller: Controller) -> bool {
        let res_ret = ffi::maa_tasker_bind_resource(self.handle, resource.handle());
        let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
        let ok = res_ret != 0 && ctrl_ret != 0;

        if ok {
            // Only hold the resource/controller when both bindings succeed
            self._resource_holder = Some(resource);
            self._controller_holder = Some(controller);
        }

        ok
    }

```

From your comment, it sounds like there might also be separate `bind_resource` and `bind_controller` methods that similarly store holders even when the FFI calls fail. You should apply the same pattern to those methods: compute an `ok` flag from the FFI return value, update the corresponding holder only when `ok` is `true`, and return `ok`. If you decide to keep holders even on failure for some reason, add Rust doc comments on those methods documenting that behavior and consider exposing an explicit "is bound" method that checks the actual FFI state (e.g. via `maa_tasker_inited` or an equivalent function).
</issue_to_address>

### Comment 5
<location> `source/binding/Rust/src/toolkit.rs:50-60` </location>
<code_context>
+    fn find_adb_devices_impl(specified_adb: Option<&Path>) -> Vec<AdbDevice> {
+        let list_handle = ffi::maa_toolkit_adb_device_list_create();
+
+        if let Some(adb_path) = specified_adb {
+            let path_str = adb_path.to_string_lossy();
+            let path_cstr = CString::new(path_str.as_ref()).unwrap();
+            ffi::maa_toolkit_adb_device_find_specified(path_cstr.as_ptr(), list_handle);
+        } else {
+            ffi::maa_toolkit_adb_device_find(list_handle);
+        }
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The return value of adb device discovery functions is ignored, which can hide discovery failures.

Since both `maa_toolkit_adb_device_find_specified` and `maa_toolkit_adb_device_find` return `MaaBool`, it would be safer to branch on that result and avoid using the list when discovery fails. For example:
```rust
let ok = if let Some(adb_path) = specified_adb { /* call _find_specified */ } else { /* call _find */ };
if ok == 0 {
    ffi::maa_toolkit_adb_device_list_destroy(list_handle);
    return Vec::new(); // or a Result
}
```
That way you only read the list when discovery succeeded and you keep error handling explicit.

```suggestion
    fn find_adb_devices_impl(specified_adb: Option<&Path>) -> Vec<AdbDevice> {
        let list_handle = ffi::maa_toolkit_adb_device_list_create();

        let ok = if let Some(adb_path) = specified_adb {
            let path_str = adb_path.to_string_lossy();
            let path_cstr = CString::new(path_str.as_ref()).unwrap();
            ffi::maa_toolkit_adb_device_find_specified(path_cstr.as_ptr(), list_handle)
        } else {
            ffi::maa_toolkit_adb_device_find(list_handle)
        };

        if ok == 0 {
            ffi::maa_toolkit_adb_device_list_destroy(list_handle);
            return Vec::new();
        }

```
</issue_to_address>

### Comment 6
<location> `source/binding/Rust/src/tasker.rs:355-356` </location>
<code_context>
+    }
+}
+
+unsafe impl Send for Tasker {}
+unsafe impl Sync for Tasker {}
+
+// Global options
</code_context>

<issue_to_address>
**question (bug_risk):** Unsafe Send/Sync impls assume the underlying C tasker handle is thread-safe.

These `unsafe impl Send` / `Sync` blocks for `Tasker` (and the similar ones for `Controller`/`Resource`) assume `MaaTaskerHandle` is fully thread-safe to move and share. If that’s not guaranteed by the C API, this can introduce data races and undefined behavior that Rust’s type system can’t prevent.

If thread-safety is guaranteed, please document that in a single obvious place (e.g., near the handle/type definition or via a dedicated wrapper type) so the assumption is explicit. If not, narrow the auto traits (e.g., only `Send`, or neither) and expose any required synchronization at a higher level in Rust instead.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据这些反馈改进后续的审查。
Original comment in English

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • Several public-facing paths can panic (load_symbol! using expect, multiple CString::new(...).unwrap() in resource.rs, tasker.rs, toolkit.rs, examples, etc.); consider returning Result from these call sites or converting the panics into MaaError so the binding behaves like a library rather than aborting the process on bad input or missing symbols.
  • Types like Tasker, Controller, Resource, and DesktopWindow are marked unsafe impl Send/Sync while holding raw FFI handles; it would be good to either justify these guarantees (e.g. in comments) or wrap underlying calls with appropriate synchronization if the C API is not explicitly thread-safe.
  • The custom_recognitions and custom_actions fields in Resource are currently never used; if registration of custom callbacks is planned, consider wiring these through to the underlying FFI now or removing the fields to avoid confusion until that support exists.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Several public-facing paths can panic (`load_symbol!` using `expect`, multiple `CString::new(...).unwrap()` in `resource.rs`, `tasker.rs`, `toolkit.rs`, examples, etc.); consider returning `Result` from these call sites or converting the panics into `MaaError` so the binding behaves like a library rather than aborting the process on bad input or missing symbols.
- Types like `Tasker`, `Controller`, `Resource`, and `DesktopWindow` are marked `unsafe impl Send/Sync` while holding raw FFI handles; it would be good to either justify these guarantees (e.g. in comments) or wrap underlying calls with appropriate synchronization if the C API is not explicitly thread-safe.
- The `custom_recognitions` and `custom_actions` fields in `Resource` are currently never used; if registration of custom callbacks is planned, consider wiring these through to the underlying FFI now or removing the fields to avoid confusion until that support exists.

## Individual Comments

### Comment 1
<location> `source/binding/Rust/src/buffer.rs:206-210` </location>
<code_context>
+        Ok(result)
+    }
+
+    pub fn set(&self, values: &[&str]) -> Result<bool> {
+        for value in values {
+            let str_buf = StringBuffer::new();
+            str_buf.set(value)?;
+            if ffi::maa_string_list_buffer_append(self.handle, str_buf.handle()) == 0 {
+                return Ok(false);
+            }
</code_context>

<issue_to_address>
**issue (bug_risk):** StringListBuffer::set drops StringBuffers after appending, likely leaving the underlying list with dangling pointers.

Each `StringBuffer` is created with `own: true` and its handle appended to the list, then `str_buf` is dropped at end of the loop, calling `maa_string_buffer_destroy` while the list still stores the pointer. That’s a likely use-after-free on the C side.

You’ll need to change the ownership model here, e.g.:
- use an `into_raw` / `from_raw` pattern so ownership is transferred to the C list, or
- build the list fully on the C side so Rust never owns the child buffers.

At minimum, construct these via `StringBuffer::from_handle` with `own = false`, or otherwise ensure list elements are not dropped by Rust.
</issue_to_address>

### Comment 2
<location> `source/binding/Rust/src/resource.rs:103-104` </location>
<code_context>
+        self.set_inference(InferenceExecutionProvider::Cpu, InferenceDevice::Cpu)
+    }
+
+    pub fn use_directml(&self, device_id: i32) -> bool {
+        self.set_inference(InferenceExecutionProvider::DirectMl, unsafe {
+            std::mem::transmute(device_id)
+        })
</code_context>

<issue_to_address>
**issue (bug_risk):** Casting `i32` to `InferenceDevice` with `transmute` is unsafe and unnecessary here.

`use_directml` (and `use_coreml`) build `InferenceDevice` with `unsafe { std::mem::transmute(device_id) }`. This is UB for any `device_id` that doesn’t match a valid enum discriminant, and it isn’t needed since the C API ultimately wants an `i32`.

Prefer either:
- keeping `ep`/`device` as plain `i32` at this FFI boundary, or
- adding a safe `InferenceDevice::from_i32 -> Option<Self>` and handling invalid values.

This avoids `transmute` while keeping the API explicit and safe.
</issue_to_address>

### Comment 3
<location> `source/binding/Rust/src/tasker.rs:126-132` </location>
<code_context>
+        ffi::maa_tasker_inited(self.handle) != 0
+    }
+
+    pub fn post_task(&self, entry: &str, pipeline_override: &Value) -> Result<TaskJob> {
+        let entry_cstr = CString::new(entry).map_err(|e| MaaError::BufferError(e.to_string()))?;
+        let override_str = serde_json::to_string(pipeline_override)?;
+        let override_cstr =
+            CString::new(override_str).map_err(|e| MaaError::BufferError(e.to_string()))?;
+
+        let id =
+            ffi::maa_tasker_post_task(self.handle, entry_cstr.as_ptr(), override_cstr.as_ptr());
+        Ok(TaskJob::new(id, self.handle))
</code_context>

<issue_to_address>
**suggestion (bug_risk):** No handling for invalid task IDs (MAA_INVALID_ID) returned from FFI when creating TaskJob.

The C API uses `MAA_INVALID_ID == 0`, but `post_task` always constructs a `TaskJob` from whatever ID is returned. If the FFI call fails and returns `MAA_INVALID_ID`, subsequent `wait`/`status` calls may misbehave or even hit UB depending on the C side.

Consider checking the result and returning an error when it equals `MAA_INVALID_ID`, similar to your null-handle checks elsewhere, e.g.:
```rust
let id = ffi::maa_tasker_post_task(self.handle, entry_cstr.as_ptr(), override_cstr.as_ptr());
if id == MAA_INVALID_ID {
    return Err(MaaError::TaskFailed); // or a more specific error
}
Ok(TaskJob::new(id, self.handle))
```
You may also want to apply the same pattern to other FFI calls that return task/controller/resource IDs if they use `MAA_INVALID_ID` as well.

Suggested implementation:

```rust
    pub fn post_task(&self, entry: &str, pipeline_override: &Value) -> Result<TaskJob> {
        let entry_cstr = CString::new(entry).map_err(|e| MaaError::BufferError(e.to_string()))?;
        let override_str = serde_json::to_string(pipeline_override)?;
        let override_cstr =
            CString::new(override_str).map_err(|e| MaaError::BufferError(e.to_string()))?;

        let id =
            ffi::maa_tasker_post_task(self.handle, entry_cstr.as_ptr(), override_cstr.as_ptr());

        if id == ffi::MAA_INVALID_ID {
            // FFI reported a failure to create the task; surface this as a Rust error
            return Err(MaaError::TaskFailed("failed to post task".to_string()));
        }

        Ok(TaskJob::new(id, self.handle))
    }

```

1. Ensure that the FFI constant `MAA_INVALID_ID` is exposed under `ffi::MAA_INVALID_ID`.  
   - If it is defined elsewhere (e.g., as a top-level constant), adjust the comparison accordingly (for example, `if id == MAA_INVALID_ID`).

2. Confirm that `MaaError::TaskFailed(String)` (or the constructor used here) exists in your error type definition.  
   - If it does not, add an appropriate variant to your `MaaError` enum (e.g. `TaskFailed(String)` or a more general FFI error) and update its `Display`/`Error` implementations as needed.  
   - If your project uses a different error style (e.g. `MaaError::TaskFailed` without payload, or `MaaError::TaskError { reason: String }`), adjust the `return Err(...)` line to match.

3. Consider applying the same `MAA_INVALID_ID` check to other FFI functions that return IDs (e.g. `maa_tasker_post_stop`) and, if appropriate, changing their return type from `TaskJob` to `Result<TaskJob>` to propagate errors instead of constructing a `TaskJob` for an invalid ID.
</issue_to_address>

### Comment 4
<location> `source/binding/Rust/src/tasker.rs:84-89` </location>
<code_context>
+        self.handle
+    }
+
+    pub fn bind(&mut self, resource: Resource, controller: Controller) -> bool {
+        let res_ret = ffi::maa_tasker_bind_resource(self.handle, resource.handle());
+        let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
+        self._resource_holder = Some(resource);
+        self._controller_holder = Some(controller);
+        res_ret != 0 && ctrl_ret != 0
+    }
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** bind/bind_resource/bind_controller store holders even when the underlying bind call fails.

Here the `Resource` and `Controller` are stored in `_resource_holder` / `_controller_holder` even if the FFI bind calls fail. That means callers can see `false` but the `Tasker` still holds the handles, which may be surprising and could mask failed bindings.

Either only store the holders when both FFI calls succeed, or keep the current behavior but document that `Tasker` may retain resources/controllers purely for lifetime management after a failed bind, and consider exposing an explicit "is bound" check (e.g., via `maa_tasker_inited`).

Suggested implementation:

```rust
    pub fn bind(&mut self, resource: Resource, controller: Controller) -> bool {
        let res_ret = ffi::maa_tasker_bind_resource(self.handle, resource.handle());
        let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
        let ok = res_ret != 0 && ctrl_ret != 0;

        if ok {
            // Only hold the resource/controller when both bindings succeed
            self._resource_holder = Some(resource);
            self._controller_holder = Some(controller);
        }

        ok
    }

```

From your comment, it sounds like there might also be separate `bind_resource` and `bind_controller` methods that similarly store holders even when the FFI calls fail. You should apply the same pattern to those methods: compute an `ok` flag from the FFI return value, update the corresponding holder only when `ok` is `true`, and return `ok`. If you decide to keep holders even on failure for some reason, add Rust doc comments on those methods documenting that behavior and consider exposing an explicit "is bound" method that checks the actual FFI state (e.g. via `maa_tasker_inited` or an equivalent function).
</issue_to_address>

### Comment 5
<location> `source/binding/Rust/src/toolkit.rs:50-60` </location>
<code_context>
+    fn find_adb_devices_impl(specified_adb: Option<&Path>) -> Vec<AdbDevice> {
+        let list_handle = ffi::maa_toolkit_adb_device_list_create();
+
+        if let Some(adb_path) = specified_adb {
+            let path_str = adb_path.to_string_lossy();
+            let path_cstr = CString::new(path_str.as_ref()).unwrap();
+            ffi::maa_toolkit_adb_device_find_specified(path_cstr.as_ptr(), list_handle);
+        } else {
+            ffi::maa_toolkit_adb_device_find(list_handle);
+        }
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The return value of adb device discovery functions is ignored, which can hide discovery failures.

Since both `maa_toolkit_adb_device_find_specified` and `maa_toolkit_adb_device_find` return `MaaBool`, it would be safer to branch on that result and avoid using the list when discovery fails. For example:
```rust
let ok = if let Some(adb_path) = specified_adb { /* call _find_specified */ } else { /* call _find */ };
if ok == 0 {
    ffi::maa_toolkit_adb_device_list_destroy(list_handle);
    return Vec::new(); // or a Result
}
```
That way you only read the list when discovery succeeded and you keep error handling explicit.

```suggestion
    fn find_adb_devices_impl(specified_adb: Option<&Path>) -> Vec<AdbDevice> {
        let list_handle = ffi::maa_toolkit_adb_device_list_create();

        let ok = if let Some(adb_path) = specified_adb {
            let path_str = adb_path.to_string_lossy();
            let path_cstr = CString::new(path_str.as_ref()).unwrap();
            ffi::maa_toolkit_adb_device_find_specified(path_cstr.as_ptr(), list_handle)
        } else {
            ffi::maa_toolkit_adb_device_find(list_handle)
        };

        if ok == 0 {
            ffi::maa_toolkit_adb_device_list_destroy(list_handle);
            return Vec::new();
        }

```
</issue_to_address>

### Comment 6
<location> `source/binding/Rust/src/tasker.rs:355-356` </location>
<code_context>
+    }
+}
+
+unsafe impl Send for Tasker {}
+unsafe impl Sync for Tasker {}
+
+// Global options
</code_context>

<issue_to_address>
**question (bug_risk):** Unsafe Send/Sync impls assume the underlying C tasker handle is thread-safe.

These `unsafe impl Send` / `Sync` blocks for `Tasker` (and the similar ones for `Controller`/`Resource`) assume `MaaTaskerHandle` is fully thread-safe to move and share. If that’s not guaranteed by the C API, this can introduce data races and undefined behavior that Rust’s type system can’t prevent.

If thread-safety is guaranteed, please document that in a single obvious place (e.g., near the handle/type definition or via a dedicated wrapper type) so the assumption is explicit. If not, narrow the auto traits (e.g., only `Send`, or neither) and expose any required synchronization at a higher level in Rust instead.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@MistEO
Copy link
Member Author

MistEO commented Nov 28, 2025

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你好——我已经审阅了你的更改,并发现一些需要处理的问题。

  • ffi.rs 中的 FFI 封装使用了带有 expect(...)load_symbol!,在符号缺失或尚未调用 load_library/load_toolkit 时会发生 panic;建议让这些公共函数(或者 framework()/toolkit() 访问器)返回 Result,这样调用方就可以优雅地处理库加载和符号解析失败,而不是直接让进程崩溃。
  • 几个核心类型(ResourceControllerTaskerDesktopWindow)基于对 C API 的注释被标记为 unsafe impl Send/Sync;更安全的做法是将这些保证进行集中和收窄(例如通过专门的封装或特性开关),并清晰地文档化哪些操作实际上是线程安全的,哪些需要外部同步,以避免在 C 端保证发生变化或被误解时出现意外的数据竞争。
  • lib.rs 中的全局库句柄(FRAMEWORK_LIB/TOOLKIT_LIB)是写一次后就不会被清除的,但当前 API 并不能阻止在同一进程的不同路径中多次调用 load_library/load_toolkit;你可能需要让后续调用具备幂等性(例如比较路径),或者返回包含已加载路径的更具描述性的错误,以帮助诊断配置错误。
面向 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- The FFI wrappers in `ffi.rs` use `load_symbol!` with `expect(...)`, which will panic on missing symbols or if `load_library`/`load_toolkit` has not been called; consider returning a `Result` from these public functions (or from the `framework()/toolkit()` accessors) so library load and symbol resolution failures can be handled gracefully by callers instead of aborting the process.
- Several core types (`Resource`, `Controller`, `Tasker`, `DesktopWindow`) are marked `unsafe impl Send/Sync` based only on comments about the C API; it would be safer to centralize and narrow these guarantees (e.g., through a dedicated wrapper or feature flag) and document clearly which operations are actually thread-safe vs. require external synchronization, to avoid accidental data races if the C side's guarantees change or are misunderstood.
- The global library handles in `lib.rs` (`FRAMEWORK_LIB`/`TOOLKIT_LIB`) are write-once and never cleared, but the API does not prevent multiple `load_library`/`load_toolkit` calls from different paths within the same process; you may want to either make subsequent calls idempotent (e.g., compare paths) or return a more descriptive error that includes the already-loaded path to help diagnose misconfiguration.

## Individual Comments

### Comment 1
<location> `source/binding/Rust/src/buffer.rs:111-112` </location>
<code_context>
+        ffi::maa_image_buffer_set_raw_data(self.handle, data.as_ptr(), width, height, img_type) != 0
+    }
+
+    pub fn to_vec(&self) -> Vec<u8> {
+        let size = (self.width() * self.height() * self.channels()) as usize;
+        if size == 0 {
+            return Vec::new();
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against negative or overflowing dimensions when computing image buffer size.

`size` is computed as `(self.width() * self.height() * self.channels()) as usize`. Because this multiplication happens in `i32`, negative values or overflow will wrap before being cast to `usize`, which can cause out-of-bounds reads when creating the slice. Please promote to a wider unsigned type before multiplying, and validate that width/height/channels are non-negative and that the product fits in `usize` before calling `from_raw_parts`.
</issue_to_address>

### Comment 2
<location> `source/binding/Rust/src/tasker.rs:335-344` </location>
<code_context>
+        }))
+    }
+
+    pub fn get_task_detail(&self, task_id: MaaTaskId) -> Result<Option<TaskDetail>> {
+        let entry_buf = StringBuffer::new();
+        let mut size: MaaSize = 0;
+        let mut status: MaaStatus = 0;
+
+        if ffi::maa_tasker_get_task_detail(
+            self.handle,
+            task_id,
+            entry_buf.handle(),
+            std::ptr::null_mut(),
+            &mut size,
+            &mut status,
+        ) == 0
+        {
+            return Ok(None);
+        }
+
+        let mut node_ids: Vec<MaaNodeId> = vec![0; size as usize];
+        if ffi::maa_tasker_get_task_detail(
+            self.handle,
</code_context>

<issue_to_address>
**nitpick (performance):** Handle the zero-node case explicitly to avoid an unnecessary second FFI call.

Right now, when `size` is 0 after the first `maa_tasker_get_task_detail` call, we still allocate an empty `Vec` and make a second FFI call with `node_ids.as_mut_ptr()`. Instead, consider an early return for `size == 0` that constructs a `TaskDetail` with an empty `nodes` vector, which keeps the logic clearer and skips the extra FFI call.
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得这些评审有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey there - I've reviewed your changes and found some issues that need to be addressed.

  • The FFI wrappers in ffi.rs use load_symbol! with expect(...), which will panic on missing symbols or if load_library/load_toolkit has not been called; consider returning a Result from these public functions (or from the framework()/toolkit() accessors) so library load and symbol resolution failures can be handled gracefully by callers instead of aborting the process.
  • Several core types (Resource, Controller, Tasker, DesktopWindow) are marked unsafe impl Send/Sync based only on comments about the C API; it would be safer to centralize and narrow these guarantees (e.g., through a dedicated wrapper or feature flag) and document clearly which operations are actually thread-safe vs. require external synchronization, to avoid accidental data races if the C side's guarantees change or are misunderstood.
  • The global library handles in lib.rs (FRAMEWORK_LIB/TOOLKIT_LIB) are write-once and never cleared, but the API does not prevent multiple load_library/load_toolkit calls from different paths within the same process; you may want to either make subsequent calls idempotent (e.g., compare paths) or return a more descriptive error that includes the already-loaded path to help diagnose misconfiguration.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The FFI wrappers in `ffi.rs` use `load_symbol!` with `expect(...)`, which will panic on missing symbols or if `load_library`/`load_toolkit` has not been called; consider returning a `Result` from these public functions (or from the `framework()/toolkit()` accessors) so library load and symbol resolution failures can be handled gracefully by callers instead of aborting the process.
- Several core types (`Resource`, `Controller`, `Tasker`, `DesktopWindow`) are marked `unsafe impl Send/Sync` based only on comments about the C API; it would be safer to centralize and narrow these guarantees (e.g., through a dedicated wrapper or feature flag) and document clearly which operations are actually thread-safe vs. require external synchronization, to avoid accidental data races if the C side's guarantees change or are misunderstood.
- The global library handles in `lib.rs` (`FRAMEWORK_LIB`/`TOOLKIT_LIB`) are write-once and never cleared, but the API does not prevent multiple `load_library`/`load_toolkit` calls from different paths within the same process; you may want to either make subsequent calls idempotent (e.g., compare paths) or return a more descriptive error that includes the already-loaded path to help diagnose misconfiguration.

## Individual Comments

### Comment 1
<location> `source/binding/Rust/src/buffer.rs:111-112` </location>
<code_context>
+        ffi::maa_image_buffer_set_raw_data(self.handle, data.as_ptr(), width, height, img_type) != 0
+    }
+
+    pub fn to_vec(&self) -> Vec<u8> {
+        let size = (self.width() * self.height() * self.channels()) as usize;
+        if size == 0 {
+            return Vec::new();
</code_context>

<issue_to_address>
**issue (bug_risk):** Guard against negative or overflowing dimensions when computing image buffer size.

`size` is computed as `(self.width() * self.height() * self.channels()) as usize`. Because this multiplication happens in `i32`, negative values or overflow will wrap before being cast to `usize`, which can cause out-of-bounds reads when creating the slice. Please promote to a wider unsigned type before multiplying, and validate that width/height/channels are non-negative and that the product fits in `usize` before calling `from_raw_parts`.
</issue_to_address>

### Comment 2
<location> `source/binding/Rust/src/tasker.rs:335-344` </location>
<code_context>
+        }))
+    }
+
+    pub fn get_task_detail(&self, task_id: MaaTaskId) -> Result<Option<TaskDetail>> {
+        let entry_buf = StringBuffer::new();
+        let mut size: MaaSize = 0;
+        let mut status: MaaStatus = 0;
+
+        if ffi::maa_tasker_get_task_detail(
+            self.handle,
+            task_id,
+            entry_buf.handle(),
+            std::ptr::null_mut(),
+            &mut size,
+            &mut status,
+        ) == 0
+        {
+            return Ok(None);
+        }
+
+        let mut node_ids: Vec<MaaNodeId> = vec![0; size as usize];
+        if ffi::maa_tasker_get_task_detail(
+            self.handle,
</code_context>

<issue_to_address>
**nitpick (performance):** Handle the zero-node case explicitly to avoid an unnecessary second FFI call.

Right now, when `size` is 0 after the first `maa_tasker_get_task_detail` call, we still allocate an empty `Vec` and make a second FFI call with `node_ids.as_mut_ptr()`. Instead, consider an early return for `size == 0` that constructs a `TaskDetail` with an empty `nodes` vector, which keeps the logic clearer and skips the extra FFI call.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an experimental Rust binding for MaaFramework, providing a type-safe, idiomatic Rust API over the C FFI. The binding is AI-generated and explicitly marked as not fully tested, requiring developer validation before production use. It follows standard interface design patterns with async job support, event notifications, and custom recognition/action capabilities.

Key changes:

  • Complete Rust binding with FFI layer wrapping all MaaFramework C APIs
  • High-level abstractions for Resource, Controller (ADB/Win32/Debug), Tasker, Context, and toolkit functions
  • Job-based async pattern for all operations (TaskJob, CtrlJob, ResJob)
  • Documentation in both English and Chinese with usage examples

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
source/binding/Rust/src/lib.rs Library root with module exports and dynamic library loading
source/binding/Rust/src/ffi.rs Raw FFI bindings to C API using libloading for dynamic symbol resolution
source/binding/Rust/src/error.rs Error types using thiserror for Result-based error handling
source/binding/Rust/src/define.rs Type definitions, enums, and constants matching C API
source/binding/Rust/src/buffer.rs Safe wrappers for string, image, rect, and list buffers
source/binding/Rust/src/job.rs Async job abstractions for tracking operation status
source/binding/Rust/src/resource.rs Resource management with pipeline and inference configuration
source/binding/Rust/src/controller.rs Device controllers for ADB, Win32, and debug modes
source/binding/Rust/src/tasker.rs Task execution with detail retrieval for recognition/actions
source/binding/Rust/src/context.rs Task context for running tasks and managing state
source/binding/Rust/src/toolkit.rs Device discovery utilities for ADB and desktop windows
source/binding/Rust/src/custom.rs Traits for custom recognition and action implementations
source/binding/Rust/src/notification.rs Event sink traits and notification message parsing
source/binding/Rust/examples/basic.rs Example demonstrating device connection and resource loading
source/binding/Rust/README.md English documentation with API reference and examples
source/binding/Rust/README.zh_cn.md Chinese documentation with quick start guide
source/binding/Rust/Cargo.toml Package manifest with dependencies (libloading, serde, thiserror)
docs/en_us/2.1-Integration.md Updated integration guide noting AI-generated experimental status

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +220 to +224
return Ok(false);
}
// Transfer ownership to the C list - don't let Rust destroy this buffer
// The C StringListBuffer takes ownership of appended buffers
str_buf.into_raw();
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StringListBuffer::set method has a memory management issue. After calling str_buf.into_raw() to transfer ownership to the C API, the code doesn't verify whether the C API actually takes ownership. If maa_string_list_buffer_append fails (returns 0), the buffer ownership has already been transferred via into_raw(), causing a memory leak.

The code should transfer ownership only after confirming the append operation succeeded:

pub fn set(&self, values: &[&str]) -> Result<bool> {
    for value in values {
        let str_buf = StringBuffer::new();
        str_buf.set(value)?;
        if ffi::maa_string_list_buffer_append(self.handle, str_buf.handle()) == 0 {
            // Don't call into_raw() here - let Drop clean up
            return Ok(false);
        }
        // Transfer ownership only after successful append
        str_buf.into_raw();
    }
    Ok(true)
}
Suggested change
return Ok(false);
}
// Transfer ownership to the C list - don't let Rust destroy this buffer
// The C StringListBuffer takes ownership of appended buffers
str_buf.into_raw();
// Don't transfer ownership; let Drop clean up
return Ok(false);
} else {
// Transfer ownership to the C list - don't let Rust destroy this buffer
// The C StringListBuffer takes ownership of appended buffers
str_buf.into_raw();
}

Copilot uses AI. Check for mistakes.
Comment on lines +94 to +102
let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
let ok = res_ret != 0 && ctrl_ret != 0;

if ok {
self._resource_holder = Some(resource);
self._controller_holder = Some(controller);
}

ok
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Tasker::bind method has inconsistent error handling. When either resource or controller binding fails, the method returns false, but the successful binding is not rolled back. This creates an inconsistent state where one is bound but not the other, and the successful binding's holder is stored even though the overall operation failed.

Consider rolling back the successful binding or ensuring both succeed atomically:

pub fn bind(&mut self, resource: Resource, controller: Controller) -> bool {
    let res_ret = ffi::maa_tasker_bind_resource(self.handle, resource.handle());
    if res_ret == 0 {
        return false;
    }
    
    let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
    if ctrl_ret == 0 {
        // Could potentially unbind resource here if API supports it
        return false;
    }
    
    self._resource_holder = Some(resource);
    self._controller_holder = Some(controller);
    true
}
Suggested change
let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
let ok = res_ret != 0 && ctrl_ret != 0;
if ok {
self._resource_holder = Some(resource);
self._controller_holder = Some(controller);
}
ok
if res_ret == 0 {
return false;
}
let ctrl_ret = ffi::maa_tasker_bind_controller(self.handle, controller.handle());
if ctrl_ret == 0 {
// Roll back resource binding if controller binding fails
// If the unbind function is available, call it; otherwise, document the limitation.
#[allow(unused_unsafe)]
unsafe {
// If ffi::maa_tasker_unbind_resource exists, call it.
#[cfg(feature = "has_unbind_resource")]
{
ffi::maa_tasker_unbind_resource(self.handle);
}
}
return false;
}
self._resource_holder = Some(resource);
self._controller_holder = Some(controller);
true

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +62
controller.post_connection().wait();

// 5. 加载资源
let resource = Resource::new()?;
resource.post_bundle("./resource").wait();

// 6. 创建任务器
let mut tasker = Tasker::new()?;
tasker.bind(resource, controller);

// 7. 执行任务
let job = tasker.post_task("StartUp", &json!({}))?;
job.wait();
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation claims that .wait() can be called without error handling (line 50, 54, 64, etc.), but this is misleading. Looking at the implementation, post_connection(), post_bundle(), and post_task() all return Result<Job>, not Job. The .wait() call would need to be chained after unwrapping the Result:

controller.post_connection()?.wait();
// or
resource.post_bundle("./resource")?.wait();

This is a documentation error that would prevent the code from compiling.

Suggested change
controller.post_connection().wait();
// 5. 加载资源
let resource = Resource::new()?;
resource.post_bundle("./resource").wait();
// 6. 创建任务器
let mut tasker = Tasker::new()?;
tasker.bind(resource, controller);
// 7. 执行任务
let job = tasker.post_task("StartUp", &json!({}))?;
job.wait();
controller.post_connection()?.wait();
// 5. 加载资源
let resource = Resource::new()?;
resource.post_bundle("./resource")?.wait();
// 6. 创建任务器
let mut tasker = Tasker::new()?;
tasker.bind(resource, controller);
// 7. 执行任务
let job = tasker.post_task("StartUp", &json!({}))?.wait();

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +28
pub fn from_handle(handle: MaaContextHandle) -> Result<Self> {
if handle.is_null() {
return Err(MaaError::InvalidHandle);
}
let tasker_handle = ffi::maa_context_get_tasker(handle);
if tasker_handle.is_null() {
return Err(MaaError::InvalidHandle);
}
Ok(Self {
handle,
tasker: Tasker::from_handle(tasker_handle),
})
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Missing validation for null handle in Context::from_handle. While the function checks if the handle is null (line 17-19) and if tasker_handle is null (lines 21-23), there's no check to ensure the context handle is actually valid. The C API might return a non-null but invalid handle in some error cases. Consider adding additional validation or documenting the assumption that non-null handles from the C API are always valid.

Copilot uses AI. Check for mistakes.

// 创建任务器
let mut tasker = Tasker::new()?;
tasker.bind(resource, controller.inner.clone());
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example code at line 70 references controller.inner.clone() but the Controller struct doesn't implement Clone, and the binding design intentionally avoids cloning handles (only one owner should drop the handle). This code will not compile.

The example should pass the controller directly or use a reference pattern:

tasker.bind(resource, controller);

Note: This assumes bind takes ownership of the controller, which appears to be the intended design based on the bind method signature.

Suggested change
tasker.bind(resource, controller.inner.clone());
tasker.bind(resource, controller);

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +114
if !tasker.bind_resource(resource) {
println!("绑定资源失败");
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential memory leak in the example code. The controller is created but then passed to tasker.bind() which takes ownership. After the bind, attempting to use controller later would fail. However, the example only creates the controller and binds it, so this is actually not an issue in the provided code.

Looking more carefully, there's a different issue: the comment on line 112 mentions "需要将 controller 的内部 handle 传递,这里简化处理" (need to pass the controller's internal handle, simplified here), but then the code only binds the resource and doesn't bind the controller at all. This leaves the tasker in an incomplete state.

The code should bind both:

if !tasker.bind(resource, controller) {
    println!("绑定失败");
}

Or bind them separately if the controller was already used:

// If controller needs to be used elsewhere, bind separately
if !tasker.bind_resource(resource) {
    println!("绑定资源失败");
}
Suggested change
if !tasker.bind_resource(resource) {
println!("绑定资源失败");
if !tasker.bind(resource, controller) {
println!("绑定失败");

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +63
controller.post_connection().wait();
if !controller.connected() {
return Err(MaaError::ConnectionFailed);
}

// 创建资源
let resource = Resource::new()?;
resource.post_bundle("path/to/resource").wait();
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error propagation operator in the example. The same compilation issue exists here - post_connection() returns Result<CtrlJob>, not CtrlJob, so .wait() cannot be called directly.

Should be:

controller.post_connection()?.wait();

This pattern is repeated throughout the examples in lines 56, 63, 116, 130-147.

Suggested change
controller.post_connection().wait();
if !controller.connected() {
return Err(MaaError::ConnectionFailed);
}
// 创建资源
let resource = Resource::new()?;
resource.post_bundle("path/to/resource").wait();
controller.post_connection()?.wait();
if !controller.connected() {
return Err(MaaError::ConnectionFailed);
}
// 创建资源
let resource = Resource::new()?;
resource.post_bundle("path/to/resource")?.wait();

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +28
pub box_: Option<Rect>,
pub raw_detail: Value,
pub raw_image: Vec<u8>,
pub draw_images: Vec<Vec<u8>>,
}

#[derive(Debug, Clone)]
pub struct ActionDetail {
pub action_id: MaaActId,
pub name: String,
pub action: Action,
pub box_: Rect,
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Inconsistent naming: the field is named box_ (with trailing underscore) throughout the codebase because box is a Rust keyword. However, this creates an inconsistent API surface. Consider using a more idiomatic name like bounding_box, bbox, or rect instead of box_.

This affects multiple structs:

  • RecognitionDetail::box_ (line 17)
  • ActionDetail::box_ (line 28)

While box_ is valid Rust, it's unconventional and may confuse users.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +13
pub struct Context {
handle: MaaContextHandle,
tasker: Tasker,
}
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Context struct stores a Tasker instance (line 12) that is created from a non-owned handle (via Tasker::from_handle). However, the Context doesn't manage the lifetime of this tasker handle, and if the original tasker is dropped, this reference would become invalid. The code should document that the Context must not outlive the original Tasker, or it should ensure proper lifetime management.

Consider adding a lifetime parameter or documenting the constraint:

/// Context must not outlive the Tasker it was created from
pub struct Context {
    handle: MaaContextHandle,
    tasker: Tasker,
}

Copilot uses AI. Check for mistakes.
@MistEO MistEO force-pushed the main branch 2 times, most recently from bef9648 to dcee917 Compare December 5, 2025 21:36
@MistEO MistEO marked this pull request as draft December 6, 2025 08:29
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.

2 participants