【Python】LUA 2

第八章:lupa 真实世界应用案例深度剖析

本章旨在超越基础示例,探讨在更复杂的真实场景中,如何有效地运用lupa来集成Python和Lua,发挥两者结合的威力。

8.1 游戏开发:Python作为引擎,Lua作为脚本

8.1.1 场景设定

假设我们正在用Python开发一个2D角色扮演游戏 (RPG)。Python负责核心渲染循环、资源管理、物理(如果需要)、网络通信和主要的游戏状态。Lua将用于:

NPC (Non-Player Character) 行为逻辑: 每个NPC的行为模式、对话、任务触发等。
任务/剧情脚本: 定义任务的目标、步骤、完成条件、奖励以及相关的剧情事件。
技能/法术效果: 定义技能的伤害计算、状态效果、冷却时间等。
UI事件处理: 某些复杂UI窗口的交互逻辑。
地图事件: 玩家进入特定区域、与特定对象交互时触发的事件。

8.1.2 架构设计考量

Lua环境管理:

单一全局Lua状态机: 简单,所有脚本共享一个环境。易于脚本间通信,但也可能导致命名冲突或状态污染。适用于中小型项目。
多个Lua状态机: 例如,每个NPC或每个重要系统(如任务系统)拥有独立的Lua状态机。隔离性好,可以并行加载或执行(如果Python侧使用线程,并为每个线程的Lua状态机加锁)。但状态机之间的通信需要通过Python中转。
每个脚本文件一个临时状态机: 用于执行一次性的脚本(如某个特定事件触发),执行完即销毁。开销较大,但隔离性最好。
折中方案: 一个主要的Lua状态机用于通用库和系统脚本,然后为某些需要高度隔离或有状态的单元(如复杂NPC)创建临时的或可复用的沙箱环境或子状态机(如果Lua支持且lupa能良好管理)。

对于我们的RPG示例,可以考虑一个主要的LuaRuntime用于加载通用的游戏API和库,然后:

NPC行为脚本可能在共享状态下运行,但通过良好的模块化和命名空间来避免冲突。
或者,每个NPC实例在Lua中是一个table,其行为函数在其元表中定义,由Python引擎统一调用其update方法。

Python到Lua的API暴露 (Binding):
Python引擎需要向Lua脚本暴露一系列API,让Lua可以查询游戏世界状态、控制角色、播放声音、显示UI等。

全局函数/Table: 将Python函数或包含多个函数代理的table注册到Lua的全局环境中。

# Python 侧
# lua = lupa.LuaRuntime()
# def python_play_sound(sound_id_str):
#     print(f"[Python Engine] Playing sound: {sound_id_str}")
#     # ... 实际播放声音的代码 ...
# lua.globals().GameAPI_PlaySound = python_play_sound

# lua.execute("""
# GameAPI_MovePlayerTo = function(x, y)
#     -- 这个是Lua侧定义的函数,它内部会调用更底层的Python API (如果需要)
#     -- 或者直接由Python提供这个函数
#     PythonEngine.player:set_position(x, y) -- 假设PythonEngine.player已暴露
# end
# """)

特定对象代理: 将Python游戏对象(如玩家角色、NPC对象)的代理直接传递给Lua。Lua脚本可以直接调用这些代理对象的方法。

# Python 侧
# class PyPlayer:
#     def __init__(self, name):
#         self.name = name
#         self.x = 0
#         self.y = 0
#     def move(self, dx, dy):
#         self.x += dx
#         self.y += dy
#         print(f"[PyPlayer {self.name}] moved to ({self.x}, {self.y})")
#     def get_health(self): return 100

# player_instance_py = PyPlayer("Hero")
# lua.globals().player_char = player_instance_py # 将Python玩家实例暴露给Lua

# Lua 侧
# -- player_char:move(10, 5)
# -- local current_health = player_char:get_health()
# -- print("Player health in Lua:", current_health)

设计原则:

最小权限: 只暴露Lua脚本完成其任务所必需的API。
清晰的命名空间: 使用table (如GameAPI.Physics.Raycast(...)) 来组织API,避免全局污染。
数据封装: 尽量让Lua通过API获取数据或请求操作,而不是直接暴露Python对象的内部状态让Lua随意修改(除非这是刻意设计的)。
性能: 避免过于频繁的细粒度跨语言调用。如果Lua需要对大量对象进行相同操作,考虑提供一个Python API,接收一个对象列表或ID列表,在Python侧批量处理。

Lua到Python的回调:
Lua脚本经常需要通知Python引擎发生了某些事件,或者请求Python执行某些Lua无法直接完成的操作。

Python对象方法调用: 如8.1.2中,Lua持有Python对象的代理,直接调用其方法。
注册回调函数: Lua脚本可以将一个Lua函数注册给Python引擎,用于响应特定事件。

# Python 侧
# class EventManager:
#     def __init__(self, lua_runtime):
#         self.lua = lua_runtime
#         self.lua_event_listeners = {} # event_name -> [lua_function_proxy, ...]

#     def register_lua_listener(self, event_name_str, lua_func_proxy):
#         if not lupa.lua_type(lua_func_proxy) == 'LUA_TFUNCTION':
#             print(f"[Python] Error: Provided listener for '{event_name_str}' is not a Lua function.")
#             return
#         if event_name_str not in self.lua_event_listeners:
#             self.lua_event_listeners[event_name_str] = []
#         self.lua_event_listeners[event_name_str].append(lua_func_proxy)
#         print(f"[Python] Lua listener registered for event: {event_name_str}")

#     def trigger_event(self, event_name_str, *args):
#         print(f"[Python] Triggering event: {event_name_str} with args: {args}")
#         if event_name_str in self.lua_event_listeners:
#             # 将Python参数转换为Lua能接收的形式
#             lua_args = [self.lua.table_from(arg) if isinstance(arg, dict) else arg for arg in args]
#             for lua_listener in self.lua_event_listeners[event_name_str]:
#                 try:
#                     lua_listener(*lua_args) # 调用Lua回调函数
#                 except lupa.LuaError as e:
#                     print(f"[Python] Error calling Lua listener for {event_name_str}: {e}")

# lua = lupa.LuaRuntime()
# event_mgr_py = EventManager(lua)
# lua.globals().EventManagerPy = event_mgr_py # 暴露事件管理器给Lua

# Lua 侧
# -- local function on_player_death_lua(event_data_table)
# --     print("[Lua CB] Player has died! Level:", event_data_table.level)
# --     print("[Lua CB] Player killer was:", event_data_table.killer_name)
# --     -- 可以在这里触发游戏结束的Lua逻辑
# -- end
# -- EventManagerPy:register_lua_listener("player_death", on_player_death_lua)

# Python 侧触发事件
# event_data = {"level": 5, "killer_name": "Dragon"}
# event_mgr_py.trigger_event("player_death", event_data)

数据交换格式:

Python dict/list 与 Lua table 之间的转换由lupa自动处理(通过lua.table_from()和代理对象)。
对于自定义的Python类实例,它们作为userdata传递给Lua。Lua通过元表方法与它们交互。
确保日期、时间、枚举等特殊数据类型在双向传递时有一致的表示和处理方式。

脚本加载与管理:

Python引擎需要一个机制来发现、加载、重载(用于开发)和执行Lua脚本。
可以按目录结构组织Lua脚本(例如,scripts/npcs/, scripts/quests/)。
提供一个主Lua入口脚本 (main.luainit.lua),由Python首先加载,这个脚本可以负责加载其他模块、设置环境等。

8.1.3 示例深化:NPC行为树与Lua

行为树 (Behavior Tree, BT) 是游戏AI中常用的技术。我们可以用Python实现行为树的核心执行器,而每个行为节点(如“巡逻”、“攻击”、“逃跑”、“播放动画”)的具体逻辑或条件判断可以用Lua实现。

Python侧 (behavior_tree_node.py, npc_controller.py):

# behavior_tree_node.py (概念)
from enum import Enum

class BTNodeStatus(Enum): # 定义行为树节点状态枚举
    SUCCESS = 1 # 成功
    FAILURE = 2 # 失败
    RUNNING = 3 # 运行中

class BTNode: # 定义行为树节点基类
    def __init__(self, lua_vm, npc_py_proxy, lua_func_name_or_obj): # 初始化方法
        self.lua = lua_vm # Lua运行时实例
        self.npc_py = npc_py_proxy # NPC的Python代理
        self.lua_tick_func = None # Lua的tick函数

        if isinstance(lua_func_name_or_obj, str): # 如果传入的是函数名字符串
            self.lua_tick_func = self.lua.globals()[lua_func_name_or_obj] # 从Lua全局获取函数
        elif lupa.lua_type(lua_func_name_or_obj) == 'LUA_TFUNCTION': # 如果传入的是Lua函数代理
            self.lua_tick_func = lua_func_name_or_obj # 直接使用Lua函数代理
        else:
            raise ValueError("lua_func_name_or_obj must be a Lua function name or proxy.") # 抛出值错误

        if not self.lua_tick_func or lupa.lua_type(self.lua_tick_func) != 'LUA_TFUNCTION': # 检查tick函数是否存在且为函数类型
            raise LookupError(f"Lua function for BTNode not found or not a function: {
              lua_func_name_or_obj}") # 抛出查找错误

    def tick(self, dt): # 定义tick方法
        """执行此节点的Lua逻辑,并返回BTNodeStatus"""
        try:
            # 调用Lua函数,传递NPC的Python代理和dt
            # Lua函数应该返回一个数字对应BTNodeStatus的枚举值
            # 或者返回一个字符串 "SUCCESS", "FAILURE", "RUNNING"
            # print(f"[Python BTNode] Ticking Lua func for NPC {self.npc_py.id if hasattr(self.npc_py, 'id') else 'N/A'}")
            lua_status_val = self.lua_tick_func(self.npc_py, dt) # 调用Lua的tick函数

            if isinstance(lua_status_val, str): # 如果返回的是字符串
                return BTNodeStatus[lua_status_val.upper()] # 将字符串转换为枚举值
            elif isinstance(lua_status_val, int) and lua_status_val in [s.value for s in BTNodeStatus]: # 如果返回的是整数且在枚举值范围内
                return BTNodeStatus(lua_status_val) # 将整数转换为枚举值
            else:
                print(f"[Python BTNode] Lua tick function returned invalid status: {
              lua_status_val}") # 打印无效状态信息
                return BTNodeStatus.FAILURE # 返回失败状态
        except lupa.LuaError as e:
            print(f"[Python BTNode] LuaError during tick: {
              e}") # 打印Lua错误信息
            return BTNodeStatus.FAILURE # 返回失败状态
        except Exception as e:
            print(f"[Python BTNode] Python error during tick: {
              e}") # 打印Python错误信息
            return BTNodeStatus.FAILURE # 返回失败状态

# 组合节点 (Selector, Sequence - Python实现核心逻辑)
class BTSelector(BTNode): # 定义选择器节点类
    def __init__(self, children_nodes): # 初始化方法
        # Selector不需要直接的Lua tick函数,它管理子节点
        self.children = children_nodes # 子节点列表

    def tick(self, dt): # 定义tick方法 (覆盖基类)
        # print("[Python BTSelector] Ticking...")
        for child_node in self.children: # 遍历子节点
            status = child_node.tick(dt) # 执行子节点的tick方法
            if status == BTNodeStatus.SUCCESS: # 如果子节点成功
                # print("[Python BTSelector] Child SUCCESS, selector SUCCESS.")
                return BTNodeStatus.SUCCESS # 选择器成功
            if status == BTNodeStatus.RUNNING: # 如果子节点运行中
                # print("[Python BTSelector] Child RUNNING, selector RUNNING.")
                return BTNodeStatus.RUNNING # 选择器运行中
        # print("[Python BTSelector] All children FAILED, selector FAILED.")
        return BTNodeStatus.FAILURE # 所有子节点失败,选择器失败

class BTSequence(BTNode): # 定义序列节点类
    def __init__(self, children_nodes): # 初始化方法
        self.children = children_nodes # 子节点列表
        self.current_child_index = 0 # 当前子节点索引

    def tick(self, dt): # 定义tick方法 (覆盖基类)
        # print(f"[Python BTSequence] Ticking... current_child_index: {self.current_child_index}")
        if self.current_child_index >= len(self.children): # 如果已处理完所有子节点
            # print("[Python BTSequence] All children SUCCESS, sequence SUCCESS. Resetting.")
            self.current_child_index = 0 # 重置索引
            return BTNodeStatus.SUCCESS # 序列成功

        child_node = self.children[self.current_child_index] # 获取当前子节点
        status = child_node.tick(dt) # 执行子节点的tick方法

        if status == BTNodeStatus.FAILURE: # 如果子节点失败
            # print("[Python BTSequence] Child FAILED, sequence FAILED. Resetting.")
            self.current_child_index = 0 # 重置索引
            return BTNodeStatus.FAILURE # 序列失败
        if status == BTNodeStatus.SUCCESS: # 如果子节点成功
            # print(f"[Python BTSequence] Child {self.current_child_index} SUCCESS, moving to next.")
            self.current_child_index += 1 # 移至下一个子节点
            # 如果这已经是最后一个孩子并且成功了,整个序列就成功了。
            # 或者,我们可以通过再次调用tick来立即尝试下一个孩子(如果dt很大)。
            # 这里我们让它在下一帧处理下一个孩子,通过返回RUNNING (除非已完成所有)
            if self.current_child_index >= len(self.children): # 如果已处理完所有子节点
                 # print("[Python BTSequence] All children SUCCESS after advancing, sequence SUCCESS. Resetting.")
                 self.current_child_index = 0 # 重置索引
                 return BTNodeStatus.SUCCESS # 序列成功
            # print("[Python BTSequence] Child SUCCESS, sequence RUNNING (has more children).")
            return BTNodeStatus.RUNNING # 序列运行中(还有更多子节点)
        # if status == BTNodeStatus.RUNNING:
        # print("[Python BTSequence] Child RUNNING, sequence RUNNING.")
        return BTNodeStatus.RUNNING # 子节点运行中,序列运行中

# npc_controller.py (概念)
class PyNPC: # 定义Python NPC类
    def __init__(self, npc_id, lua_vm, behavior_tree_root_node_lua): # 初始化方法
        self.id = npc_id # NPC ID
        self.lua = lua_vm # Lua运行时实例
        self.x = random.randint(0, 100) # 随机x坐标
        self.y = random.randint(0, 100) # 随机y坐标
        self.target_x = self.x # 目标x坐标
        self.target_y = self.y # 目标y坐标
        self.speed = 10 # 移动速度
        self.current_action = "idle" # 当前动作
        self.health = 100 # 生命值
        self.bt_root = self._build_behavior_tree(behavior_tree_root_node_lua) # 构建行为树
        print(f"[PyNPC {
              self.id}] Created at ({
              self.x}, {
              self.y}). BT root: {
              type(self.bt_root)}") # 打印创建信息

    def _build_behavior_tree(self, bt_description_lua_table): # 定义构建行为树的方法
        """
        从Lua table描述构建行为树。
        bt_description_lua_table 示例:
        { type="selector", children = {
            { type="sequence", children = {
                {type="action", func="lua_IsHealthLow"},
                {type="action", func="lua_Flee"}
            }},
            { type="action", func="lua_Patrol" }
        }}
        """
        node_type = bt_description_lua_table.type # 获取节点类型
        # print(f"[PyNPC {self.id}] Building BT node: type={node_type}")

        if node_type == "action": # 如果节点类型是action
            # lua_func_name = bt_description_lua_table.func
            # lua_func_proxy = self.lua.globals()[lua_func_name]
            return BTNode(self.lua, self, bt_description_lua_table.func) # 创建行为节点
        elif node_type == "selector": # 如果节点类型是selector
            children_py_nodes = [] # 初始化子Python节点列表
            if bt_description_lua_table.children: # 如果有子节点描述
                for i in range(1, len(bt_description_lua_table.children) + 1): # Lua table 索引从1开始
                    child_desc_lua = bt_description_lua_table.children[i] # 获取子节点描述
                    children_py_nodes.append(self._build_behavior_tree(child_desc_lua)) # 递归构建子节点
            return BTSelector(children_py_nodes) # 创建选择器节点
        elif node_type == "sequence": # 如果节点类型是sequence
            children_py_nodes = [] # 初始化子Python节点列表
            if bt_description_lua_table.children: # 如果有子节点描述
                for i in range(1, len(bt_description_lua_table.children) + 1): # Lua table 索引从1开始
                    child_desc_lua = bt_description_lua_table.children[i] # 获取子节点描述
                    children_py_nodes.append(self._build_behavior_tree(child_desc_lua)) # 递归构建子节点
            return BTSequence(children_py_nodes) # 创建序列节点
        else:
            raise ValueError(f"Unknown BT node type: {
              node_type}") # 抛出值错误

    def update_ai(self, dt): # 定义更新AI的方法
        if self.bt_root: # 如果行为树根节点存在
            # print(f"[PyNPC {self.id}] Ticking BT root.")
            status = self.bt_root.tick(dt) # 执行行为树的tick方法
            # print(f"[PyNPC {self.id}] BT tick status: {status.name}")
        # Python侧也可以根据BT状态更新NPC的其他状态

    # --- API 供Lua调用 ---
    def move_to(self, x, y): # 定义移动到目标位置的方法
        self.target_x = x # 设置目标x坐标
        self.target_y = y # 设置目标y坐标
        self.current_action = f"moving_to_({
              x},{
              y})" # 更新当前动作为移动
        # print(f"[PyNPC {self.id}] API call: move_to({x}, {y})")
        return True # 返回True表示API调用成功

    def play_animation(self, anim_name_str): # 定义播放动画的方法
        self.current_action = f"anim_{
              anim_name_str}" # 更新当前动作为播放动画
        print(f"[PyNPC {
              self.id}] API call: play_animation('{
              anim_name_str}')") # 打印API调用信息
        return True # 返回True

    def get_position_tuple(self): # 定义获取位置元组的方法
        return self.x, self.y # 返回x, y坐标

    def is_target_reached(self): # 定义判断是否到达目标的方法
        return abs(self.x - self.target_x) < 1 and abs(self.y - self.target_y) < 1 # 判断是否到达目标

    def get_id(self): # 定义获取ID的方法
        return self.id # 返回NPC ID

    def get_health_percentage(self): # 定义获取生命值百分比的方法
        return self.health / 100.0 # 返回生命值百分比

    # 模拟物理更新
    def _physics_update(self, dt): # 定义物理更新方法 (内部使用)
        if not self.is_target_reached(): # 如果未到达目标
            dx = self.target_x - self.x # 计算x方向差值
            dy = self.target_y - self.y # 计算y方向差值
            dist = (dx*dx + dy*dy)**0.5 # 计算距离
            if dist < self.speed * dt: # 如果距离小于速度乘以时间差
                self.x = self.target_x # 到达目标x坐标
                self.y = self.target_y # 到达目标y坐标
            else:
                self.x += (dx / dist) * self.speed * dt # 更新x坐标
                self.y += (dy / dist) * self.speed * dt # 更新y坐标

Lua侧 (npc_behaviors.lua):

-- npc_behaviors.lua

-- Action Node: 检查NPC血量是否低
-- npc_py_proxy: 是Python的PyNPC实例的代理
-- dt: delta time
function lua_IsHealthLow(npc_py_proxy, dt)
    local health_percent = npc_py_proxy:get_health_percentage()
    -- print(string.format("[Lua BT lua_IsHealthLow] NPC '%s', Health: %.2f", npc_py_proxy:get_id(), health_percent))
    if health_percent < 0.3 then
        -- print("[Lua BT lua_IsHealthLow] Health is LOW. SUCCESS.")
        return "SUCCESS" -- CPython枚举BTNodeStatus.SUCCESS的值
    else
        -- print("[Lua BT lua_IsHealthLow] Health is OK. FAILURE.")
        return "FAILURE" -- BTNodeStatus.FAILURE
    end
end

-- Action Node: NPC逃跑行为
function lua_Flee(npc_py_proxy, dt)
    -- print(string.format("[Lua BT lua_Flee] NPC '%s' is fleeing!", npc_py_proxy:get_id()))
    npc_py_proxy:play_animation("flee") -- 调用Python NPC的方法播放动画

    -- 简单的逃跑逻辑:向随机方向移动一小段距离
    -- (真实游戏中会更复杂,比如远离敌人)
    local current_x, current_y = npc_py_proxy:get_position_tuple()
    local flee_target_x = current_x + math.random(-50, 50)
    local flee_target_y = current_y + math.random(-50, 50)
    npc_py_proxy:move_to(flee_target_x, flee_target_y)

    -- 假设逃跑动作总是需要一些时间来“完成”或“持续”
    -- 如果逃跑是一个持续动作,这里可以返回 RUNNING 直到满足某个条件
    -- 为了简单,我们让它立即成功,假设move_to启动了一个异步移动
    return "SUCCESS"
end

local patrol_points = {
             -- 定义巡逻点列表
    {
            x=10, y=10},
    {
            x=90, y=10},
    {
            x=90, y=90},
    {
            x=10, y=90}
}
local current_patrol_index = {
            } -- 每个NPC的当前巡逻点索引, key是npc_id

-- Action Node: NPC巡逻行为
function lua_Patrol(npc_py_proxy, dt)
    local npc_id = npc_py_proxy:get_id()
    -- print(string.format("[Lua BT lua_Patrol] NPC '%s' is patrolling.", npc_id))

    if current_patrol_index[npc_id] == nil then -- 如果NPC没有当前巡逻点索引
        current_patrol_index[npc_id] = 1 -- 初始化为第一个巡逻点
        -- print(string.format("[Lua BT lua_Patrol] NPC '%s' starting patrol at index 1.", npc_id))
    end

    if npc_py_proxy:is_target_reached() then -- 如果NPC已到达当前巡逻目标点
        -- print(string.format("[Lua BT lua_Patrol] NPC '%s' reached patrol point %d. Moving to next.", npc_id, current_patrol_index[npc_id]))
        current_patrol_index[npc_id] = (current_patrol_index[npc_id] % #patrol_points) + 1 -- 移至下一个巡逻点
        local next_point = patrol_points[current_patrol_index[npc_id]] -- 获取下一个巡逻点
        npc_py_proxy:move_to(next_point.x, next_point.y) -- 命令NPC移动到下一个点
        npc_py_proxy:play_animation("walk") -- 播放行走动画
        return "RUNNING" -- 正在前往新地点,所以是RUNNING
    else
        -- 还没有到达当前巡逻点,继续移动
        -- print(string.format("[Lua BT lua_Patrol] NPC '%s' en route to patrol point %d. RUNNING.", npc_id, current_patrol_index[npc_id]))
        return "RUNNING" -- 持续RUNNING直到到达目标
    end
    -- 此巡逻逻辑通常会一直返回 RUNNING,除非被行为树更高层打断
    -- 或者可以设计成巡逻一圈后返回SUCCESS,但这取决于BT如何设计
end

-- 为特定NPC类型定义行为树结构 (Lua table)
NPC_BEHAVIOR_TREES = {
             -- 定义NPC行为树表
    Grunt = {
             -- Grunt类型的NPC行为树
        type = "selector", -- 根节点是选择器
        children = {
             -- 子节点列表
            {
             type = "sequence", -- 第一个子节点是序列
              children = {
             -- 序列的子节点列表
                  {
            type="action", func="lua_IsHealthLow"}, -- 动作节点:检查血量是否低
                  {
            type="action", func="lua_Flee"} -- 动作节点:逃跑
              }
            },
            {
             type="action", func="lua_Patrol" } -- 第二个子节点是动作节点:巡逻
        }
    },
    --可以为其他NPC类型定义更多行为树...
    Guard = {
             -- Guard类型的NPC行为树
        type = "action", func="lua_Patrol" -- 守卫只会巡逻
    }
}

Python主游戏循环部分 (示意):

# main_game.py
# from npc_controller import PyNPC (假设已导入)
# from behavior_tree_node import BTNode, BTSelector, BTSequence, BTNodeStatus (假设已导入)
# import lupa
# import time
# import random
# import os

def run_game_with_lua_ai(): # 定义运行带Lua AI的游戏的函数
    lua = lupa.LuaRuntime(unpack_returned_tuples=True) # 创建Lua运行时实例
    try:
        # 加载Lua行为脚本
        lua_script_path = os.path.join(os.path.dirname(__file__), "npc_behaviors.lua") # 获取Lua脚本路径
        with open(lua_script_path, 'r', encoding='utf-8') as f: # 打开Lua脚本文件
            lua.execute(f.read()) # 执行Lua脚本
        print("[GameMain] Lua NPC behavior script loaded.") # 打印加载成功信息
    except Exception as e:
        print(f"[GameMain] Failed to load Lua script: {
              e}") # 打印加载失败信息
        return # 返回

    lua_globals = lua.globals() # 获取Lua全局命名空间
    npc_bt_definitions_lua = lua_globals.NPC_BEHAVIOR_TREES # 获取NPC行为树定义

    # 创建NPC实例
    npcs_py = [] # 初始化Python NPC列表
    grunt_bt_def = npc_bt_definitions_lua.Grunt # 获取Grunt类型的行为树定义
    if grunt_bt_def: # 如果定义存在
        npcs_py.append(PyNPC("Grunt01", lua, grunt_bt_def)) # 创建Grunt NPC实例
        npcs_py.append(PyNPC("Grunt02", lua, grunt_bt_def)) # 创建Grunt NPC实例
    else:
        print("[GameMain] Warning: Grunt BT definition not found in Lua.") # 打印警告信息

    guard_bt_def = npc_bt_definitions_lua.Guard # 获取Guard类型的行为树定义
    if guard_bt_def: # 如果定义存在
         npcs_py.append(PyNPC("Guard_A", lua, guard_bt_def)) # 创建Guard NPC实例
    else:
        print("[GameMain] Warning: Guard BT definition not found in Lua.") # 打印警告信息


    # 游戏循环
    last_tick_time = time.monotonic() # 获取上一帧时间
    running_duration = 15 # 游戏运行时间 (秒)
    game_start_time = time.monotonic() # 获取游戏开始时间

    print("
[GameMain] Starting AI simulation loop...") # 打印开始AI模拟循环信息
    frame_count = 0 # 初始化帧计数器
    try:
        while time.monotonic() - game_start_time < running_duration: # 当游戏时间未结束时循环
            current_time = time.monotonic() # 获取当前时间
            dt = current_time - last_tick_time # 计算时间差
            last_tick_time = current_time # 更新上一帧时间

            # 更新所有NPC的AI (通过行为树)
            for npc in npcs_py: # 遍历所有Python NPC
                npc.update_ai(dt) # 更新NPC的AI
                npc._physics_update(dt) # 更新NPC的物理状态 (内部方法,为了演示移动)

            # 模拟其他游戏逻辑和渲染 (此处简化为打印)
            if frame_count % 120 == 0 : # 大约每2秒打印一次 (假设60FPS)
                print(f"
--- Game State at t={
              time.monotonic() - game_start_time:.2f}s (dt={
              dt:.4f}) ---") # 打印游戏状态信息
                for npc in npcs_py: # 遍历所有Python NPC
                    print(f"  NPC '{
              npc.id}': Pos=({
              npc.x:.1f},{
              npc.y:.1f}), Health={
              npc.health}%, Action='{
              npc.current_action}'") # 打印NPC信息
            frame_count += 1 # 帧计数器加1
            time.sleep(max(0, 0.016 - dt)) # 维持约60FPS
    except KeyboardInterrupt:
        print("[GameMain] Simulation interrupted by user.") # 打印用户中断信息
    finally:
        print("[GameMain] AI simulation loop finished.") # 打印AI模拟循环结束信息

if __name__ == "__main__": # 如果作为主模块运行
    # 为了能运行这个示例,需要将 PyNPC, BTNode 等类定义放在一个或多个Python文件中,
    # 并确保 npc_behaviors.lua 在同一目录下或正确路径下。
    # 此处假设这些类已定义并在作用域内。
    # 这是一个结构性演示,直接运行可能需要将类定义合并或正确导入。
    run_game_with_lua_ai() # 运行带Lua AI的游戏

代码解释 (Python – 行为树部分):

BTNodeStatus Enum: 定义行为树节点可能返回的状态。
BTNode Class:

基类,每个实例代表行为树中的一个叶子节点(动作节点)。
__init__接收Lua运行时、NPC的Python代理、以及在Lua中对应的行为函数的名称(或函数代理本身)。它会获取这个Lua函数的代理self.lua_tick_func
tick(dt): 调用存储的self.lua_tick_func,将NPC的Python代理和dt作为参数传给Lua。它期望Lua函数返回一个字符串或整数,对应BTNodeStatus

BTSelector, BTSequence Classes:

Python实现了这些组合节点的控制流逻辑。
它们的tick方法会迭代其子节点(BTNode实例),并根据Selector(任一子成功则成功,任一子运行则运行,全失败则失败)或Sequence(所有子成功则成功,任一子失败则失败,任一子运行则运行)的规则来决定自身状态。

PyNPC Class:

__init__: 创建NPC时,会调用_build_behavior_tree
_build_behavior_tree(bt_description_lua_table): 这是一个关键的递归函数。它接收一个从Lua传来的table,这个table描述了行为树的结构(节点类型如”action”, “selector”, “sequence”,以及子节点和对应的Lua函数名)。它根据这个描述在Python中构建实际的BTNode, BTSelector, BTSequence实例树。
update_ai(dt): 每帧调用行为树根节点的tick方法。
提供了一系列API方法 (move_to, play_animation, get_position_tuple, is_target_reached, get_id, get_health_percentage) 供其行为树中的Lua动作节点回调。
_physics_update: 模拟NPC的移动。

代码解释 (Lua – npc_behaviors.lua):

lua_IsHealthLow, lua_Flee, lua_Patrol: 这些是Lua函数,对应行为树中的具体动作。

它们都接收npc_py_proxy (Python PyNPC实例的代理) 和 dt
它们通过npc_py_proxy调用Python NPC对象的方法来查询状态 (如:get_health_percentage()) 或执行动作 (如:move_to(), :play_animation())。
它们返回字符串 “SUCCESS”, “FAILURE”, 或 “RUNNING”,由Python端的BTNode转换为BTNodeStatus枚举。
lua_Patrol使用了一个Lua table current_patrol_index来存储每个NPC的巡逻状态(以NPC ID为键),展示了如何在Lua中维护AI状态。

NPC_BEHAVIOR_TREES Table: 这是一个Lua table,用纯数据的方式定义了不同NPC类型(如”Grunt”, “Guard”)的行为树结构。Python端的PyNPC._build_behavior_tree会读取这个table来动态构建行为树。

代码解释 (Python – 主游戏循环):

run_game_with_lua_ai():

初始化lupa,加载npc_behaviors.lua
从Lua全局获取NPC_BEHAVIOR_TREES定义。
创建PyNPC实例时,将对应的Lua行为树定义table (grunt_bt_def, guard_bt_def)传递给PyNPC的构造函数,后者会用它来构建内部的Python行为树对象实例。
游戏循环中,对每个NPC调用npc.update_ai(dt)(触发BT执行)和npc._physics_update(dt)(模拟移动)。
定期打印NPC状态。

这种深度集成的优势:

清晰的职责划分: Python负责BT的结构和执行流控制,Lua负责具体行为节点的原子逻辑。
数据驱动AI: NPC的行为树结构本身是在Lua中用数据(table)定义的 (NPC_BEHAVIOR_TREES),Python动态解析这个数据来构建树。这意味着游戏设计师可以在Lua中调整行为树结构,而无需修改Python代码。
可扩展性: 添加新的行为节点只需要在Lua中编写对应的动作函数,并在行为树定义table中引用它。
Lua的灵活性: Lua动作函数可以实现复杂的逻辑、访问游戏世界API、管理自身状态。

8.1.4 游戏中的其他Lua应用点

对话系统: Lua脚本可以定义对话树、条件分支、以及对话中触发的动作(如给予物品、更新任务状态)。Python调用Lua函数启动一段对话,Lua通过回调Python API来显示文本、选项,并接收玩家选择。
物品和技能系统:

物品属性、使用效果 (on_use脚本)。
技能的施法条件、目标选择逻辑、伤害/效果计算公式、视觉/声音效果触发等都可以在Lua中定义。

UI脚本: 复杂UI窗口(如商店、角色属性面板)的打开、关闭、数据填充、按钮点击响应等。
Mod支持: 如果游戏支持玩家创建Mod,Lua是一个非常流行的Mod脚本语言。Python引擎需要提供安全的沙箱环境和清晰的Modding API给Lua。

8.2 lupa 在可配置系统与嵌入式逻辑中的应用

这里的“嵌入式”不一定指硬件嵌入式,更多是指将Lua脚本作为一种可动态加载和配置的逻辑单元嵌入到更大的Python应用程序中,用于处理特定任务、自定义行为或实现晚期绑定逻辑。

8.2.1 场景设定:数据校验与转换服务

假设我们有一个Python服务,接收各种来源的数据负载(payloads),需要在将数据存储或进一步处理之前,对其进行校验和转换。这些校验规则和转换逻辑可能因数据来源、数据类型或业务需求的变化而频繁变更。使用硬编码的Python逻辑会导致频繁修改和重新部署主服务。

通过嵌入Lua脚本,我们可以将这些易变的规则外部化。

8.2.2 架构设计

Python核心服务:

负责接收数据(如通过HTTP API、消息队列)。
管理Lua脚本的加载和执行环境。
为Lua脚本提供必要的上下文信息和回调函数(如果Lua需要查询外部系统或执行Python辅助操作)。
处理Lua脚本执行的结果(校验通过/失败、转换后的数据)。

Lua脚本:

每个脚本或一组脚本对应特定的数据类型或来源的校验/转换规则。
Lua脚本接收数据记录(通常是table形式)和上下文作为输入。
执行校验逻辑,可以返回布尔值、错误信息列表或修改后的数据。
执行转换逻辑,返回转换后的数据。

Lua脚本示例 (validator_type_A.lua):

-- validator_type_A.lua
-- 用于校验和转换 "TypeA" 数据记录的规则

-- @param record_table Lua table, 代表待处理的数据记录
-- @param context_table Lua table, 由Python传入的上下文信息 (可选)
-- @return isValid (boolean), result_or_errors (table or string)
function validate_and_transform(record_table, context_table)
    local errors = {
            } -- 初始化错误列表

    -- 校验规则 1: 'id' 字段必须存在且为字符串
    if record_table.id == nil or type(record_table.id) ~= "string" then
        table.insert(errors, {
            field="id", message="ID is missing or not a string"}) -- 添加错误信息
    elseif string.len(record_table.id) < 3 then -- 如果ID长度小于3
        table.insert(errors, {
            field="id", message="ID must be at least 3 characters long"}) -- 添加错误信息
    end

    -- 校验规则 2: 'value' 字段必须是数字且大于0
    if record_table.value == nil or type(record_table.value) ~= "number" then
        table.insert(errors, {
            field="value", message="Value is missing or not a number"}) -- 添加错误信息
    elseif record_table.value <= 0 then -- 如果值小于等于0
        table.insert(errors, {
            field="value", message="Value must be greater than 0"}) -- 添加错误信息
    end

    -- 校验规则 3: 'category' 必须是预定义的值之一 (从上下文中获取)
    local allowed_categories = context_table and context_table.allowed_categories or {
            "CAT1", "CAT2", "DEFAULT"} -- 获取允许的类别列表
    local category_valid = false -- 初始化类别有效标志
    if record_table.category and type(record_table.category) == "string" then -- 如果类别存在且为字符串
        for _, cat in ipairs(allowed_categories) do -- 遍历允许的类别
            if record_table.category == cat then -- 如果记录类别等于允许的类别
                category_valid = true -- 设置类别有效标志为true
                break -- 跳出循环
            end
        end
    end
    if not category_valid then -- 如果类别无效
        table.insert(errors, {
             -- 添加错误信息
            field="category",
            message="Category '" .. tostring(record_table.category) .. "' is not allowed. Allowed: " .. table.concat(allowed_categories, ", ")
        })
    end

    -- 转换规则 1: 如果 'timestamp' 存在且是数字 (Unix timestamp),转换为ISO格式字符串
    -- 假设 context_table 包含一个由Python提供的 format_unix_timestamp_iso 函数代理
    if record_table.timestamp and type(record_table.timestamp) == "number" then -- 如果时间戳存在且为数字
        if context_table and context_table.py_format_timestamp then -- 如果上下文中存在Python格式化时间戳函数
            local success, iso_date_or_err = pcall(context_table.py_format_timestamp, record_table.timestamp) -- 调用Python函数
            if success then -- 如果调用成功
                record_table.iso_timestamp = iso_date_or_err -- 设置ISO格式时间戳
            else
                table.insert(errors, {
            field="timestamp", message="Failed to format timestamp via Python: " .. tostring(iso_date_or_err)}) -- 添加错误信息
            end
        else
            -- Lua 内置的简单实现 (可能不完整或不精确)
            -- record_table.iso_timestamp = os.date("!%Y-%m-%dT%TZ", record_table.timestamp) -- 使用os.date格式化 (os库需安全暴露)
            record_table.iso_timestamp = "lua_formatted_date_placeholder" -- 占位符
        end
    end

    -- 转换规则 2: 添加一个处理标记
    record_table.processed_by_lua_script = "validator_type_A.lua" -- 添加处理标记

    if #errors > 0 then -- 如果错误列表不为空
        return false, errors -- 返回false和错误列表
    else
        return true, record_table -- 返回true和处理后的记录
    end
end

-- 主入口函数,Python将调用这个
-- (或者Python直接查找 validate_and_transform)
function process(record, context)
    -- print("[Lua validator_type_A] Processing record ID:", record.id)
    return validate_and_transform(record, context) -- 调用校验和转换函数
end

Python核心服务 (data_service.py):

import lupa
import os
import datetime # 用于Python侧的时间戳转换

class LuaScriptRunner: # 定义Lua脚本运行器类
    def __init__(self, script_folder_path): # 初始化方法
        self.script_folder = script_folder_path # Lua脚本文件夹路径
        self.lua_states = {
            }  # 缓存LuaRuntime实例,每个脚本文件一个 (可选优化)
        self.py_utils_for_lua = {
             # 定义供Lua调用的Python工具函数
            "py_format_timestamp": self._python_format_timestamp_iso # 格式化时间戳函数
        }
        print(f"[Py LuaScriptRunner] Initialized. Script folder: {
              self.script_folder}") # 打印初始化信息

    def _get_lua_state_for_script(self, script_name_lua_file): # 定义获取脚本的Lua状态机的方法
        """
        为指定的脚本文件获取或创建一个LuaRuntime实例。
        并加载脚本。
        """
        if script_name_lua_file in self.lua_states: # 如果脚本已在缓存中
            return self.lua_states[script_name_lua_file] # 返回缓存的Lua状态机

        lua = lupa.LuaRuntime(unpack_returned_tuples=False) # 创建新的Lua运行时实例 (False确保多返回值是元组)
        script_path = os.path.join(self.script_folder, script_name_lua_file) # 获取脚本完整路径
        if not os.path.exists(script_path): # 如果脚本路径不存在
            raise FileNotFoundError(f"Lua script not found: {
              script_path}") # 抛出文件未找到错误

        with open(script_path, 'r', encoding='utf-8') as f: # 打开脚本文件
            lua_script_content = f.read() # 读取脚本内容
        lua.execute(lua_script_content) # 执行Lua脚本
        print(f"[Py LuaScriptRunner] Loaded Lua script: {
              script_name_lua_file}") # 打印加载成功信息

        self.lua_states[script_name_lua_file] = lua # 将Lua状态机存入缓存
        return lua # 返回Lua状态机

    @staticmethod
    def _python_format_timestamp_iso(unix_timestamp_num): # 定义Python格式化时间戳为ISO格式的静态方法
        """Python辅助函数,将Unix时间戳转换为ISO 8601字符串。"""
        if not isinstance(unix_timestamp_num, (int, float)): # 如果时间戳不是数字
            raise TypeError("Timestamp must be a number.") # 抛出类型错误
        try:
            dt_object = datetime.datetime.fromtimestamp(unix_timestamp_num, tz=datetime.timezone.utc) # 从时间戳创建datetime对象
            return dt_object.isoformat() # 返回ISO格式字符串
        except Exception as e:
            # print(f"[Py _python_format_timestamp_iso] Error formatting: {e}") # 打印格式化错误信息 (调试用)
            raise ValueError(f"Invalid timestamp value for conversion: {
              unix_timestamp_num} - {
              e}") # 抛出值错误

    def run_script_process_function(self, script_name_lua_file, record_py_dict, additional_context_py_dict=None): # 定义运行脚本处理函数的方法
        """
        加载(如果需要)并执行指定Lua脚本中的 'process' 函数。
        """
        try:
            lua = self._get_lua_state_for_script(script_name_lua_file) # 获取脚本的Lua状态机
            lua_process_func = lua.globals().process # 获取Lua的process函数
            if not lua_process_func or lupa.lua_type(lua_process_func) != 'LUA_TFUNCTION': # 检查函数是否存在且为函数类型
                raise LookupError(f"'process' function not found in Lua script: {
              script_name_lua_file}") # 抛出查找错误

            lua_record = lua.table_from(record_py_dict) # 将Python记录字典转换为Lua table

            # 创建Lua上下文,合并通用Python工具和特定调用的附加上下文
            lua_context = lua.table_from(self.py_utils_for_lua) # 从Python工具函数创建Lua上下文
            if additional_context_py_dict: # 如果有附加上下文
                for k, v in additional_context_py_dict.items(): # 遍历附加上下文项
                    # 需要确保v也能被正确转换为Lua能用的类型
                    if isinstance(v, (list, tuple, dict)): # 如果值是列表、元组或字典
                        lua_context[k] = lua.table_from(v) # 将其转换为Lua table
                    else:
                        lua_context[k] = v # 否则直接赋值
            # print(f"[Py LuaScriptRunner] Lua context for script '{script_name_lua_file}': {lua_context}")

            # 调用Lua的process函数,它应该返回一个元组 (isValid_bool, result_or_errors_table)
            # 由于 unpack_returned_tuples=False,这里会得到一个元组
            lua_result_tuple = lua_process_func(lua_record, lua_context) # 调用Lua的process函数

            if not isinstance(lua_result_tuple, tuple) or len(lua_result_tuple) != 2: # 如果返回结果不是长度为2的元组
                raise lupa.LuaError(f"Lua script '{
              script_name_lua_file}' process function did not return 2 values.") # 抛出Lua错误

            is_valid_lua_bool, result_or_errors_lua_proxy = lua_result_tuple # 解包返回结果

            # 将Lua的布尔值和代理对象转换为Python类型
            is_valid_py_bool = bool(is_valid_lua_bool) # 将Lua布尔值转换为Python布尔值

            py_result_data = None # 初始化Python结果数据
            if lupa.lua_type(result_or_errors_lua_proxy) == 'LUA_TTABLE': # 如果返回的是Lua table
                # 需要递归地将Lua table代理转换为Python dict/list
                # 为了简单,我们这里假设lupa的代理可以直接用于后续处理或序列化
                # 如果需要纯Python对象,需要一个转换函数
                py_result_data = {
            } # 初始化Python结果字典
                for k, v_proxy in result_or_errors_lua_proxy.items(): # 遍历Lua table代理项
                    # 简化转换:这里不处理深层嵌套的 table 代理到纯 python dict 的转换
                    py_result_data[k] = v_proxy # 直接赋值代理
                # 如果是错误列表,每个元素也可能是table proxy
                if not is_valid_py_bool: # 如果校验无效
                    # 假设错误是 {field="...", message="..."} 的列表
                    errors_list_py = [] # 初始化Python错误列表
                    for i in range(1, len(result_or_errors_lua_proxy) + 1): # Lua table 索引从1开始
                        err_item_proxy = result_or_errors_lua_proxy[i] # 获取错误项代理
                        if lupa.lua_type(err_item_proxy) == 'LUA_TTABLE': # 如果错误项是Lua table
                            errors_list_py.append({
            k_err: v_err for k_err, v_err in err_item_proxy.items()}) # 将错误项转换为Python字典并添加到列表
                        else:
                            errors_list_py.append(str(err_item_proxy)) # 否则转换为字符串并添加到列表
                    py_result_data = errors_list_py # 更新Python结果数据为错误列表
            elif result_or_errors_lua_proxy is not None : # 如果返回的不是Lua table但也不是nil
                 py_result_data = str(result_or_errors_lua_proxy) # 转换为字符串
            # 如果 result_or_errors_lua_proxy 是 nil (例如,Lua返回 (true, nil)),py_result_data 会是 None

            return is_valid_py_bool, py_result_data # 返回Python布尔值和结果数据

        except lupa.LuaError as e: # 捕获Lua错误
            print(f"[Py LuaScriptRunner] LuaError running script '{
              script_name_lua_file}': {
              e}") # 打印Lua错误信息
            return False, {
            "_script_error": str(e)} # 返回False和错误信息
        except FileNotFoundError as e: # 捕获文件未找到错误
            print(f"[Py LuaScriptRunner] FileNotFoundError for script '{
              script_name_lua_file}': {
              e}") # 打印文件未找到信息
            return False, {
            "_script_load_error": str(e)} # 返回False和错误信息
        except Exception as e: # 捕获其他Python错误
            print(f"[Py LuaScriptRunner] Python error running script '{
              script_name_lua_file}': {
              e}") # 打印Python错误信息
            import traceback # 导入traceback模块
            # traceback.print_exc() # 打印完整的堆栈跟踪 (调试用)
            return False, {
            "_python_error": str(e)} # 返回False和错误信息


def main_data_processing_flow(): # 定义主数据处理流程函数
    # 假设 validator_type_A.lua 在 ./lua_scripts/ 目录下
    script_dir = os.path.join(os.path.dirname(__file__), "lua_scripts_for_validation") # 获取脚本目录路径
    if not os.path.exists(script_dir): # 如果脚本目录不存在
        os.makedirs(script_dir) # 创建脚本目录
        # 创建一个示例 validator_type_A.lua 文件以便运行
        example_lua_content = """
function validate_and_transform(record_table, context_table)
    local errors = {}
    if not record_table.name then table.insert(errors, {field="name", message="Name is required"}) end
    record_table.processed_by_lua_script = "validator_type_A_example.lua"
    if #errors > 0 then return false, errors else record_table.value = (record_table.value or 0) + 10; return true, record_table end
end
function process(record, context) return validate_and_transform(record, context) end
        """
        with open(os.path.join(script_dir, "validator_type_A.lua"), "w", encoding="utf-8") as f: # 打开示例Lua文件
            f.write(example_lua_content) # 写入示例内容
        print(f"Created example Lua script at {
              os.path.join(script_dir, 'validator_type_A.lua')}") # 打印创建信息

    runner = LuaScriptRunner(script_dir) # 创建LuaScriptRunner实例

    # 示例数据记录 (Python字典)
    record1_py = {
            "id": "item001", "value": 150, "category": "CAT1", "timestamp": time.time() - 3600} # 记录1
    record2_py = {
            "value": -10, "category": "CAT_INVALID", "description": "Bad record"} # 记录2 (id缺失, value无效, category无效)
    record3_py = {
            "id": "ok003", "value": 75, "category": "CAT2", "status": "active", "timestamp": time.time()} # 记录3

    data_to_process = [record1_py, record2_py, record3_py] # 待处理数据列表
    script_to_use = "validator_type_A.lua" # 要使用的脚本

    # 附加上下文信息,可以针对每次调用或每个脚本类型动态生成
    additional_context = {
             # 附加上下文
        "allowed_categories": ["CAT1", "CAT2", "SPECIAL"], # 允许的类别
        "current_user_role": "admin" # 当前用户角色
    }

    print(f"
--- Processing records using '{
              script_to_use}' ---") # 打印处理记录信息
    for i, record_data in enumerate(data_to_process): # 遍历待处理数据
        print(f"
[Py Main] Processing record {
              i+1}: {
              record_data}") # 打印正在处理的记录
        is_valid, result = runner.run_script_process_function(script_to_use, record_data, additional_context) # 运行脚本处理函数

        if is_valid: # 如果校验有效
            print(f"[Py Main] Record {
              i+1} VALID. Transformed data: {
              result}") # 打印有效记录和转换后的数据
        else:
            print(f"[Py Main] Record {
              i+1} INVALID. Errors/Details: {
              result}") # 打印无效记录和错误详情

if __name__ == "__main__": # 如果作为主模块运行
    # 确保 main_data_processing_flow 中创建的示例脚本能被找到
    # 并且 LuaScriptRunner 初始化时传入的 script_folder_path 是正确的
    # 此处直接调用,假设目录结构符合预期
    main_data_processing_flow() # 运行主数据处理流程

代码解释 (validator_type_A.lua):

validate_and_transform(record_table, context_table):

接收代表单条数据记录的record_table和Python传入的context_table
执行一系列校验规则,并将错误信息(包含字段名和消息的table)收集到errors列表中。
使用context_table中的信息(如allowed_categories, py_format_timestamp Python函数代理)来进行更动态的校验和转换。
如果py_format_timestamp存在,它会通过pcall安全地调用这个Python函数代理。
record_table进行一些转换(如添加iso_timestamp, processed_by_lua_script)。
最后根据errors列表是否为空,返回一个布尔值表示校验是否成功,以及处理后的record_tableerrors列表。

process(record, context): 一个简单的包装函数,作为Python调用的主入口点。

代码解释 (data_service.py):

LuaScriptRunner Class:

__init__: 存储脚本文件夹路径,并定义了一个py_utils_for_lua字典,其中包含Python辅助函数的引用(如_python_format_timestamp_iso),这些函数将被传递给Lua脚本的上下文。
_get_lua_state_for_script: 管理LuaRuntime实例。它可以为每个脚本文件缓存一个LuaRuntime实例,避免重复加载和编译(如果脚本不经常变动)。如果脚本会变,则不应缓存或需要实现热重载逻辑。
_python_format_timestamp_iso: 一个Python静态方法,演示了如何将Python功能暴露给Lua使用。
run_script_process_function:

获取或创建对应脚本的LuaRuntime
获取Lua脚本中的process全局函数。
将输入的Python记录字典record_py_dictlua.table_from()转换为Lua table。
构建Lua调用的lua_context,它合并了通用的self.py_utils_for_lua和特定于此次调用的additional_context_py_dict
调用lua_process_func(lua_record, lua_context)。由于LuaRuntime创建时设置了unpack_returned_tuples=False,这里会收到一个包含两个返回值的Python元组。
解析Lua返回的元组,将Lua布尔值转换为Python布尔值,并将Lua table代理(代表结果数据或错误列表)转换为Python字典/列表(此处做了简化转换,实际可能需要更深的递归转换)。
返回Python格式的(is_valid, result_data)
包含详细的错误捕获。

main_data_processing_flow():

设置脚本目录,并创建一个示例validator_type_A.lua文件(如果不存在),以便脚本能运行。
创建LuaScriptRunner实例。
准备示例数据和附加上下文。
循环处理数据,调用runner.run_script_process_function()
根据返回结果打印校验状态和数据。

8.2.3 优势与考量

业务逻辑分离: 将易变的校验和转换规则从核心Python代码中分离出来,存放在Lua脚本中。
动态更新: Lua脚本可以独立于主Python服务进行更新和部署(需要实现脚本的热重载机制)。
可读性与DSL: 对于某些特定领域的规则,Lua的语法可能更接近领域特定语言(DSL),方便领域专家阅读和编写。
安全性: 如果Lua脚本来自不可信来源,或希望限制其能力,可以对Lua执行环境进行沙箱化,只暴露必要的上下文和安全的API。
性能:

对于非常大量的记录和极其复杂的规则,频繁的Python-Lua交互和数据转换可能会成为瓶颈。
LuaJIT可以显著提升Lua脚本自身的执行速度。
批量处理:如果可能,一次向Lua传递一批记录进行处理,而不是逐条传递,以减少调用开销。lupa可以传递列表的列表(Lua中是table的table)。

Lua脚本管理: 需要一套机制来管理Lua脚本的版本、部署和测试。
调试: 跨语言调试可能比单一语言复杂。Lua侧可以使用print或集成外部Lua调试器(如果lupa环境允许)。

8.3 Python应用的插件化架构与lupa

插件化架构允许第三方开发者或用户扩展核心应用程序的功能,而无需修改核心代码。Lua由于其轻量、易嵌入和相对安全的特性,常被用作插件的脚本语言。Python主应用提供插件API,Lua插件实现这些API来提供新功能。

8.3.1 场景设定:一个通用的文本编辑器,支持Lua插件

Python核心编辑器: 提供基本的文本编辑功能、UI框架、文件操作等。
插件API: Python核心暴露一组API,允许插件:

注册新的菜单项、工具栏按钮。
响应编辑器事件(如文件打开、保存、文本修改)。
访问和修改编辑器缓冲区的内容。
显示自定义UI(如对话框、侧边栏面板)。

Lua插件: 以.lua文件形式存在,实现特定的编辑器增强功能,如:

代码片段插入。
文本自动格式化。
集成外部工具(如Linter、编译器)。
自定义快捷键行为。

8.3.2 架构设计

插件管理器 (Python):

负责发现插件(如扫描特定目录下的.lua文件)。
为每个插件创建一个独立的Lua环境(或共享一个但有良好命名空间隔离的沙箱环境)。
向每个插件的Lua环境注入核心编辑器API的代理。
调用插件定义的生命周期函数(如plugin_load(), plugin_unload(), plugin_get_metadata())。
管理插件注册的命令、事件监听器等。

编辑器API (Python -> Lua):
一组Python类和函数,其代理被传递给Lua。

# editor_api.py (Python)
class EditorBufferAPI: # 定义编辑器缓冲区API类
    def __init__(self, buffer_id, editor_core): # 初始化方法
        self.buffer_id = buffer_id # 缓冲区ID
        self.editor_core = editor_core # 编辑器核心实例
    def get_text(self): return self.editor_core.get_buffer_text(self.buffer_id) # 获取缓冲区文本
    def set_text(self, text_str): self.editor_core.set_buffer_text(self.buffer_id, text_str) # 设置缓冲区文本
    def get_selection(self): return self.editor_core.get_selection(self.buffer_id) # 获取选区
    # ...更多缓冲区操作API
    def get_line_count(self): return len(self.get_text().splitlines()) # 获取行数

class EditorUIAPI: # 定义编辑器UI API类
    def __init__(self, editor_core): # 初始化方法
        self.editor_core = editor_core # 编辑器核心实例
    def show_message_dialog(self, title_str, message_str): self.editor_core.ui_show_message(title_str, message_str) # 显示消息对话框
    def add_menu_item(self, menu_path_str, item_name_str, lua_callback_func_proxy): # 添加菜单项
        self.editor_core.ui_add_menu(menu_path_str, item_name_str, lua_callback_func_proxy) # 调用编辑器核心添加菜单
    # ...更多UI操作API

class EditorAppAPI: # 定义编辑器应用API类
    def __init__(self, editor_core): # 初始化方法
        self.editor_core = editor_core # 编辑器核心实例
    def get_current_buffer(self): # 获取当前缓冲区API
        buffer_id = self.editor_core.get_current_buffer_id() # 获取当前缓冲区ID
        if buffer_id: # 如果缓冲区ID存在
            return EditorBufferAPI(buffer_id, self.editor_core) # 返回编辑器缓冲区API实例
        return None # 返回None
    def register_event_listener(self, event_name_str, lua_callback_func_proxy): # 注册事件监听器
        self.editor_core.events_register(event_name_str, lua_callback_func_proxy) # 调用编辑器核心注册事件
    # ...更多应用级API

# Mockup EditorCore (Python)
class MockEditorCore: # 定义模拟编辑器核心类
    def __init__(self): # 初始化方法
        self.buffers = {
              "buf1": "Hello from buffer 1
Second line."} # 初始化缓冲区
        self.current_buffer_id_val = "buf1" # 当前缓冲区ID
        self.lua_callbacks_for_menu = {
              } # 存储菜单项的Lua回调
        self.lua_event_handlers = {
              } # 存储事件的Lua处理程序
        print("[Py EditorCore] MockEditorCore initialized.") # 打印初始化信息

    def get_buffer_text(self, bid): return self.buffers.get(bid, "") # 获取缓冲区文本
    def set_buffer_text(self, bid, text): self.buffers[bid] = text; print(f"[Py EditorCore] Buffer '{
                bid}' set to: '{
                text[:30]}...'") # 设置缓冲区文本
    def get_selection(self, bid): return (0,0) # 模拟获取选区
    def get_current_buffer_id(self): return self.current_buffer_id_val # 获取当前缓冲区ID
    def ui_show_message(self, title, msg): print(f"[Py EditorCore UI] Message: '{
                title}' - '{
                msg}'") # 显示消息
    def ui_add_menu(self, menu_path, item_name, lua_cb): # 添加菜单项
        key = f"{
                menu_path}/{
                item_name}" # 生成菜单项键
        print(f"[Py EditorCore UI] Adding menu item: {
                key} (Lua callback type: {
                lupa.lua_type(lua_cb)})") # 打印添加菜单项信息
        self.lua_callbacks_for_menu[key] = lua_cb # 存储Lua回调
    def events_register(self, event_name, lua_cb): # 注册事件
        if event_name not in self.lua_event_handlers: self.lua_event_handlers[event_name] = [] # 如果事件名不存在,则初始化为空列表
        self.lua_event_handlers[event_name].append(lua_cb) # 添加Lua回调到事件处理程序列表
        print(f"[Py EditorCore Events] Registered Lua listener for event '{
                event_name}'") # 打印注册监听器信息

    def simulate_menu_click(self, menu_key): # 模拟菜单点击
        if menu_key in self.lua_callbacks_for_menu: # 如果菜单键存在
            print(f"[Py EditorCore] Simulating click on menu '{
                menu_key}'...") # 打印模拟点击信息
            try:
                self.lua_callbacks_for_menu[menu_key]() # 调用Lua回调 (假设无参数)
            except lupa.LuaError as e:
                print(f"Error in Lua menu callback for '{
                menu_key}': {
                e}") # 打印Lua错误信息
        else:
            print(f"[Py EditorCore] No Lua callback for menu '{
                menu_key}'") # 打印无回调信息

    def simulate_event(self, event_name, data_py_dict): # 模拟事件触发
        if event_name in self.lua_event_handlers: # 如果事件名存在
            print(f"[Py EditorCore] Simulating event '{
                event_name}' with data: {
                data_py_dict}") # 打印模拟事件信息
            # 需要将 data_py_dict 转换为 Lua table 传递给 Lua 回调
            # (需要访问创建回调的那个LuaRuntime实例来做转换)
            # 这里简化,假设回调不需要复杂参数或插件管理器会处理转换
            for lua_cb in self.lua_event_handlers[event_name]: # 遍历Lua回调
                try:
                    # lua_data_table = lua_runtime_of_plugin.table_from(data_py_dict) # 正确的做法
                    lua_cb(data_py_dict) # 简化:直接传递Python字典,Lua侧lupa会自动尝试转换
                except lupa.LuaError as e:
                    print(f"Error in Lua event callback for '{
                event_name}': {
                e}") # 打印Lua错误信息

Lua插件脚本:
每个插件是一个Lua文件,通常会定义一些元数据函数和事件处理函数。

示例Lua插件 (plugins/my_formatter.lua):

-- plugins/my_formatter.lua
-- 一个简单的文本格式化插件

local plugin_name = "MyAutoFormatter" -- 插件名称
local plugin_version = "0.1" -- 插件版本

-- 由Python插件管理器调用
function plugin_get_metadata()
    return {
              name = plugin_name, version = plugin_version, author = "AI Assistant"} -- 返回插件元数据
end

-- 由Python插件管理器调用,在插件加载时执行
-- editor_api_proxy 是Python传入的 EditorAppAPI 的代理
function plugin_load(editor_api_proxy)
    print(string.format("[Lua Plugin %s] Loaded. Editor API proxy type: %s", plugin_name, type(editor_api_proxy))) -- 打印加载信息

    -- 注册一个菜单项
    local menu_path = "Tools/Formatters" -- 菜单路径
    local item_name = "Format Current Buffer (Lua)" -- 菜单项名称
    editor_api_proxy.ui:add_menu_item(menu_path, item_name, on_format_buffer_menu_click) -- 添加菜单项,并指定回调函数

    -- 注册一个事件监听器:当文件打开时
    editor_api_proxy:register_event_listener("file_opened", on_file_opened_event) -- 注册文件打开事件监听器

    print(string.format("[Lua Plugin %s] Registered menu item and event listener.", plugin_name)) -- 打印注册信息
    return true -- 返回true表示加载成功
end

-- 由Python插件管理器调用,在插件卸载时执行 (可选)
function plugin_unload()
    print(string.format("[Lua Plugin %s] Unloaded.", plugin_name)) -- 打印卸载信息
    -- 在这里可以进行清理工作,如注销监听器 (如果API支持)
    return true -- 返回true
end

-- Lua回调函数:当 "Format Current Buffer" 菜单项被点击时
-- editor_api_proxy 是全局的(如果 plugin_load 时设置了)或者通过某种方式可访问
-- 更好的做法是 editor_api_proxy.ui:add_menu_item 能够接受一个闭包或者在调用时能获取到 editor_api_proxy
-- 这里为了简单,假设 editor_api_proxy 是 plugin_load 时传入的那个,并且我们能访问到它
-- (或者Python在调用此回调时,会将 editor_api_proxy 作为参数传入)
-- 假设Python在调用时会提供 editor_api_proxy
function on_format_buffer_menu_click(editor_api_from_python_cb)
    print(string.format("[Lua Plugin %s] Menu item 'Format Buffer' clicked.", plugin_name)) -- 打印菜单项点击信息
    local E = editor_api_from_python_cb -- 使用Python回调时传入的API代理

    local current_buffer = E:get_current_buffer() -- 获取当前缓冲区API
    if current_buffer then -- 如果当前缓冲区存在
        local original_text = current_buffer:get_text() -- 获取原始文本
        print(string.format("[Lua Plugin %s] Original text length: %d", plugin_name, string.len(original_text))) -- 打印原始文本长度

        -- 非常简单的格式化:将每行前后的空格去掉,并将多个空格替换为单个空格
        local lines = {
              } -- 初始化行列表
        for line in string.gmatch(original_text, "([^
]*)
?") do -- 逐行匹配文本
            line = string.gsub(line, "^%s+", "") -- 去掉行首空格
            line = string.gsub(line, "%s+$", "") -- 去掉行尾空格
            line = string.gsub(line, "%s+", " ") -- 将多个空格替换为单个空格
            table.insert(lines, line) -- 将处理后的行添加到列表
        end
        local formatted_text = table.concat(lines, "
") -- 用换行符连接行

        current_buffer:set_text(formatted_text) -- 设置格式化后的文本
        E.ui:show_message_dialog(plugin_name, "Buffer formatted by Lua plugin!") -- 显示消息对话框
    else
        E.ui:show_message_dialog(plugin_name .. " Error", "No active buffer to format.") -- 显示错误消息对话框
    end
end

-- Lua回调函数:当 "file_opened" 事件发生时
-- event_data_table 是Python事件系统传递过来的数据
function on_file_opened_event(editor_api_from_python_cb, event_data_table)
    local E = editor_api_from_python_cb -- API代理
    local file_path = event_data_table and event_data_table.path or "Unknown path" -- 获取文件路径
    print(string.format("[Lua Plugin %s] Event 'file_opened': %s", plugin_name, file_path)) -- 打印事件信息

    local line_count = E:get_current_buffer() and E:get_current_buffer():get_line_count() or 0 -- 获取行数
    E.ui:show_message_dialog(plugin_name, "File '".. file_path .."' opened. Lines: " .. line_count) -- 显示消息对话框
end

print(string.format("[Lua Plugin %s] Script parsed.", plugin_name)) -- 打印脚本解析完成信息

Python插件管理器 (plugin_manager.py):

import lupa
import os
# from editor_api import EditorAppAPI, MockEditorCore # 假设这些已定义并可导入

class LuaPluginManager: # 定义Lua插件管理器类
    def __init__(self, plugin_folder_path_str, editor_core_instance_py): # 初始化方法
        self.plugin_folder = plugin_folder_path_str # 插件文件夹路径
        self.editor_core = editor_core_instance_py # 编辑器核心实例
        self.loaded_plugins = {
            } # plugin_id -> {lua_runtime, editor_api_proxy_py, metadata_lua}
        print(f"[Py PluginManager] Initialized. Plugin folder: {
              self.plugin_folder}") # 打印初始化信息

    def discover_and_load_plugins(self): # 定义发现并加载插件的方法
        print(f"[Py PluginManager] Discovering plugins in {
              self.plugin_folder}...") # 打印发现插件信息
        if not os.path.isdir(self.plugin_folder): # 如果插件文件夹不是目录
            print(f"[Py PluginManager] Plugin folder does not exist: {
              self.plugin_folder}") # 打印文件夹不存在信息
            return # 返回

        for filename in os.listdir(self.plugin_folder): # 遍历插件文件夹中的文件
            if filename.endswith(".lua"): # 如果文件以.lua结尾
                plugin_id = os.path.splitext(filename)[0] # 获取插件ID (文件名不含扩展名)
                plugin_script_path = os.path.join(self.plugin_folder, filename) # 获取插件脚本路径
                print(f"[Py PluginManager] Found potential plugin: {
              plugin_id} at {
              plugin_script_path}") # 打印发现插件信息
                self._load_single_plugin(plugin_id, plugin_script_path) # 加载单个插件

    def _load_single_plugin(self, plugin_id_str, script_path_str): # 定义加载单个插件的方法
        try:
            lua = lupa.LuaRuntime(unpack_returned_tuples=True, register_eval=False) # 创建独立的Lua运行时实例,禁用python.eval
            # 沙箱化: 可以创建一个受限的全局环境,只注入必要的API
            # sandbox_globals = lua.table()
            # sandbox_globals.print = lua.globals().print -- 允许 print
            # -- 注入API到沙箱
            # editor_api_for_plugin_lua = self._create_editor_api_for_lua(lua)
            # sandbox_globals.EditorAPI = editor_api_for_plugin_lua
            # lua.execute(script_content, env=sandbox_globals)

            # 简单起见,这里不完全沙箱化,但会显式传递API对象
            with open(script_path_str, 'r', encoding='utf-8') as f: # 打开脚本文件
                script_content = f.read() # 读取脚本内容
            lua.execute(script_content) # 在新的Lua状态机中执行插件脚本

            # 创建并注入编辑器API代理
            editor_api_proxy_for_plugin_py = EditorAppAPI(self.editor_core) # 创建编辑器应用API实例
            # 使Python的API对象在Lua中可访问,同时让Lua函数能回调到这个特定的API实例
            # 我们会在调用 plugin_load 等函数时,将这个 editor_api_proxy_for_plugin_py 作为参数传入

            # 获取插件元数据
            lua_get_metadata_func = lua.globals().plugin_get_metadata # 获取Lua的plugin_get_metadata函数
            metadata_lua_table = None # 初始化Lua元数据表
            if lua_get_metadata_func and lupa.lua_type(lua_get_metadata_func) == 'LUA_TFUNCTION': # 如果函数存在且为函数类型
                metadata_lua_table = lua_get_metadata_func() # 调用Lua函数获取元数据
                print(f"[Py PluginManager] Plugin '{
              plugin_id_str}' metadata: Name='{
              metadata_lua_table.name}', Ver='{
              metadata_lua_table.version}'") # 打印元数据信息
            else:
                print(f"[Py PluginManager] Warning: plugin_get_metadata not found or invalid in '{
              plugin_id_str}'.") # 打印警告信息

            # 调用插件的加载函数
            lua_load_func = lua.globals().plugin_load # 获取Lua的plugin_load函数
            if lua_load_func and lupa.lua_type(lua_load_func) == 'LUA_TFUNCTION': # 如果函数存在且为函数类型
                # 将EditorAppAPI的实例传递给Lua的plugin_load函数
                # Lua侧的回调(如菜单点击)需要一种方式能再次拿到这个API实例
                # 解决方案1: plugin_load 将API实例存为Lua全局或上值 (不推荐全局)
                # 解决方案2: Python在调用Lua回调时,总是将对应的API实例作为第一个参数传入
                #              (示例 my_formatter.lua 采用了这种预期)
                load_success = lua_load_func(editor_api_proxy_for_plugin_py) # 调用Lua的plugin_load函数
                if load_success: # 如果加载成功
                    self.loaded_plugins[plugin_id_str] = {
             # 将插件信息存入加载的插件字典
                        "lua_runtime": lua, # Lua运行时实例
                        "editor_api_proxy": editor_api_proxy_for_plugin_py, # 编辑器API代理
                        "metadata": metadata_lua_table # Lua元数据表
                    }
                    print(f"[Py PluginManager] Plugin '{
              plugin_id_str}' loaded successfully.") # 打印加载成功信息
                else:
                    print(f"[Py PluginManager] Plugin '{
              plugin_id_str}' load function returned false or nil.") # 打印加载失败信息
            else:
                print(f"[Py PluginManager] Warning: plugin_load not found or invalid in '{
              plugin_id_str}'.") # 打印警告信息

        except lupa.LuaError as e: # 捕获Lua错误
            print(f"[Py PluginManager] LuaError loading plugin '{
              plugin_id_str}': {
              e}") # 打印Lua错误信息
        except FileNotFoundError: # 捕获文件未找到错误
            print(f"[Py PluginManager] Script file not found for plugin '{
              plugin_id_str}': {
              script_path_str}") # 打印文件未找到信息
        except Exception as e: # 捕获其他Python错误
            print(f"[Py PluginManager] Python error loading plugin '{
              plugin_id_str}': {
              e}") # 打印Python错误信息
            import traceback # 导入traceback模块
            # traceback.print_exc() # 打印完整的堆栈跟踪

    def unload_plugin(self, plugin_id_str): # 定义卸载插件的方法
        if plugin_id_str in self.loaded_plugins: # 如果插件已加载
            plugin_info = self.loaded_plugins[plugin_id_str] # 获取插件信息
            lua = plugin_info["lua_runtime"] # 获取Lua运行时实例
            editor_api_proxy = plugin_info["editor_api_proxy"] # 获取编辑器API代理
            lua_unload_func = lua.globals().plugin_unload # 获取Lua的plugin_unload函数

            if lua_unload_func and lupa.lua_type(lua_unload_func) == 'LUA_TFUNCTION': # 如果函数存在且为函数类型
                try:
                    lua_unload_func(editor_api_proxy) # 调用Lua的plugin_unload函数 (传入API代理,以备不时之需)
                    print(f"[Py PluginManager] Plugin '{
              plugin_id_str}' unload function called.") # 打印卸载函数调用信息
                except lupa.LuaError as e:
                    print(f"[Py PluginManager] LuaError during plugin '{
              plugin_id_str}' unload: {
              e}") # 打印Lua错误信息
            
            # 清理:理论上,当lua (LuaRuntime) 对象被GC时,其lua_State会关闭
            # 如果不再持有对 plugin_info["lua_runtime"] 的引用,它最终会被回收
            del self.loaded_plugins[plugin_id_str] # 从加载的插件字典中删除插件信息
            print(f"[Py PluginManager] Plugin '{
              plugin_id_str}' unloaded and runtime scheduled for GC.") # 打印卸载完成信息
            # TODO: 还需要从编辑器核心注销此插件注册的所有菜单、事件监听器等
            # 这需要编辑器API支持反注册,或者插件管理器记录所有注册并进行清理。
        else:
            print(f"[Py PluginManager] Plugin '{
              plugin_id_str}' not currently loaded.") # 打印插件未加载信息

def run_editor_with_plugins(): # 定义运行带插件的编辑器的函数
    # 创建模拟的编辑器核心
    editor_core = MockEditorCore() # 创建模拟编辑器核心实例

    # 设置插件目录 (假设在当前文件同级下有一个 'plugins' 文件夹)
    plugins_dir = os.path.join(os.path.dirname(__file__), "plugins") # 获取插件目录路径
    if not os.path.exists(plugins_dir): # 如果插件目录不存在
        os.makedirs(plugins_dir) # 创建插件目录
        # 创建一个示例 my_formatter.lua 文件以便运行
        example_plugin_content = """
function plugin_get_metadata() return {name="ExamplePlugin", version="0.0"} end
function plugin_load(api) print("[Lua ExamplePlugin] Loaded, API:", api); api.ui:show_message_dialog("Example", "Example Plugin Loaded!"); return true end
function plugin_unload() print("[Lua ExamplePlugin] Unloaded.") return true end
        """ # 示例插件内容
        with open(os.path.join(plugins_dir, "my_formatter_example.lua"), "w", encoding="utf-8") as f: # 打开示例插件文件
            f.write(example_plugin_content) # 写入示例内容
        print(f"Created example plugin script at {
              os.path.join(plugins_dir, 'my_formatter_example.lua')}") # 打印创建信息

    # 初始化并加载插件
    plugin_mgr = LuaPluginManager(plugins_dir, editor_core) # 创建Lua插件管理器实例
    plugin_mgr.discover_and_load_plugins() # 发现并加载插件

    print("
--- Simulating Editor Usage ---") # 打印模拟编辑器使用信息
    # 模拟用户点击由 'my_formatter.lua' (如果加载了) 注册的菜单项
    # 假设 my_formatter.lua 插件加载时,将 editor_api_proxy_for_plugin_py 存了起来,
    # 或者 Python 在调用时会正确地将 API 代理作为参数传给 Lua 回调。
    # 在我们的 my_formatter.lua 中,on_format_buffer_menu_click 期望 editor_api 作为参数。
    # MockEditorCore.simulate_menu_click 需要改进来传递这个。

    # 为了让模拟点击工作,我们需要让 MockEditorCore 在调用Lua回调时传递 editor_api_proxy
    # 这意味着 MockEditorCore.ui_add_menu 时不仅要存 lua_cb,还要存对应的 editor_api_proxy
    # 或者,让 Lua 回调函数成为一个闭包,捕获 plugin_load 时传入的 editor_api_proxy。

    # 简化模拟:直接从 plugin_mgr 获取特定插件的 API 代理,并手动调用
    if "my_formatter_example" in plugin_mgr.loaded_plugins: # 如果示例插件已加载
        formatter_plugin_info = plugin_mgr.loaded_plugins["my_formatter_example"] # 获取插件信息
        # lua_runtime = formatter_plugin_info["lua_runtime"]
        # on_format_menu_click_lua = lua_runtime.globals().on_format_buffer_menu_click # (假设函数名)
        # if on_format_menu_click_lua:
            # on_format_menu_click_lua(formatter_plugin_info["editor_api_proxy"]) # 手动调用

        # 更好的模拟方式是 MockEditorCore.simulate_menu_click 知道要传递哪个API代理
        # 或者 Lua 回调是这样注册的:
        # editor_api_proxy.ui:add_menu_item(path, name, function() on_format_buffer_menu_click(editor_api_proxy) end)
        # 即,在Lua中创建一个新的闭包来捕获 editor_api_proxy。

        # 我们当前的 my_formatter.lua 期望 editor_api 作为参数。
        # MockEditorCore 的 simulate_menu_click 现在不传递这个。
        # 为了演示,我们假设 editor_core.simulate_menu_click 被修改了,或者回调能找到 API。
        editor_core.simulate_menu_click("Tools/Formatters/Format Current Buffer (Lua)") # 模拟菜单点击

        # 模拟事件触发
        editor_core.simulate_event("file_opened", {
            "path": "/path/to/some/document.txt"}) # 模拟文件打开事件

    # 卸载插件
    plugin_mgr.unload_plugin("my_formatter_example") # 卸载示例插件
    # plugin_mgr.unload_plugin("another_plugin_id") # 卸载其他插件

if __name__ == "__main__": # 如果作为主模块运行
    # 同样,确保 EditorAppAPI, MockEditorCore 等类已定义或导入
    run_editor_with_plugins() # 运行带插件的编辑器

代码解释 (editor_api.py – Python API 定义):

EditorBufferAPI, EditorUIAPI, EditorAppAPI: 这些Python类封装了编辑器核心功能,它们的实例(或其方法的代理)将被传递给Lua插件。
MockEditorCore: 一个简化的编辑器核心实现,用于演示。它有名义上的缓冲区、UI方法和事件注册机制。

ui_add_menu: 存储Lua回调函数代理。
events_register: 存储Lua事件处理函数代理。
simulate_menu_click, simulate_event: 用于从Python侧触发,进而调用存储的Lua回调。

重要: 在simulate_menu_clicksimulate_event中,当调用Lua回调时,理想情况下应该将创建该回调时对应的EditorAppAPI实例(或其相关部分)作为参数传递给Lua回调函数,或者Lua回调本身是一个闭包,已经捕获了它需要的API代理。示例中的my_formatter.lua期望API代理作为参数传入其回调。MockEditorCore的模拟触发部分需要完善才能完全匹配这种期望。

代码解释 (my_formatter.lua – Lua插件):

plugin_get_metadata(): 返回插件的基本信息。
plugin_load(editor_api_proxy):

接收Python传入的editor_api_proxy (一个EditorAppAPI实例的lupa代理)。
通过editor_api_proxy.ui:add_menu_item(...)注册一个菜单项,并将Lua函数on_format_buffer_menu_click作为回调。
通过editor_api_proxy:register_event_listener(...)注册一个事件监听器,将Lua函数on_file_opened_event作为回调。

plugin_unload(): 可选的清理函数。
on_format_buffer_menu_click(editor_api_from_python_cb)on_file_opened_event(editor_api_from_python_cb, event_data_table):

这些是Lua回调函数。它们期望第一个参数是Python在调用它们时传入的EditorAppAPI的代理 (标记为editor_api_from_python_cb)。
它们使用这个API代理来与编辑器核心交互(如获取缓冲区文本、设置文本、显示消息)。
on_file_opened_event还接收一个event_data_table,包含事件相关数据。

代码解释 (plugin_manager.py – Python插件管理器):

LuaPluginManager Class:

__init__: 存储插件目录和编辑器核心实例。
discover_and_load_plugins(): 扫描插件目录,对每个.lua文件调用_load_single_plugin
_load_single_plugin():

为每个插件创建一个独立的lupa.LuaRuntime实例。这提供了良好的隔离性,一个插件的错误或全局变量不会影响其他插件。
执行插件的Lua脚本。
创建一个EditorAppAPI实例 (editor_api_proxy_for_plugin_py),将其作为参数传递给Lua插件的plugin_load函数。
调用Lua插件的plugin_get_metadataplugin_load
将成功加载的插件信息(包括其独立的LuaRuntime实例和API代理)存储在self.loaded_plugins中。

unload_plugin():

如果插件已加载,调用其Lua脚本中的plugin_unload函数。
self.loaded_plugins中移除该插件的信息。当Python不再持有对该插件的LuaRuntime实例的引用时,它会被GC,对应的Lua状态机也会被关闭。
重要: 真正的卸载还需要从编辑器核心中移除该插件注册的所有UI元素和事件监听器。这部分逻辑需要在EditorCoreEditorAppAPI中支持反注册,并由PluginManager在卸载时调用。

8.3.3 插件化架构的优势与挑战

可扩展性: 核心应用功能可以通过外部Lua脚本轻松扩展。
社区贡献: 易于第三方开发者为应用创建和分享插件。
隔离性: 为每个插件使用独立的LuaRuntime可以提供较好的隔离,防止插件间冲突。
动态性: 插件通常可以在运行时加载、卸载甚至更新(需要更复杂的管理)。
API设计是关键: 一个良好、稳定且文档清晰的Python到Lua的插件API是成功的核心。API需要仔细考虑安全性、易用性和性能。
安全性 (沙箱): 如果插件来自不可信来源,必须在严格的沙箱环境中执行Lua脚本,限制其对文件系统、网络等的访问,并只暴露安全的API子集。在_load_single_plugin中创建LuaRuntime后,应配置其环境。
资源管理: 确保插件卸载时,其占用的所有资源(Lua状态机、Python侧注册的回调等)都被正确释放。
版本控制: 核心API和插件之间可能存在版本兼容性问题。需要有机制来处理(例如,API版本化,插件声明其依赖的API版本)。
性能: 大量插件或设计不良的API调用可能影响核心应用性能。

第九章:lupa 与 LuaJIT 深度集成:释放FFI的潜能

LuaJIT以其卓越的性能闻名,这主要归功于其即时编译器(JIT)。除了JIT编译Lua代码外,LuaJIT还提供了一个非常强大的FFI库,允许Lua代码直接调用外部C函数和操作C数据结构,几乎无需编写任何C绑定代码。当lupa针对LuaJIT编译时,Python应用就能间接利用这一强大特性。

9.1 前提条件:lupa 针对 LuaJIT 编译

要使用LuaJIT的FFI特性,你的lupa Python包必须是在构建时链接到LuaJIT库的。

检查:

import lupa

try:
    lua = lupa.LuaRuntime() # 创建Lua运行时实例
    lua_version_str = lua.globals()._VERSION # 获取Lua版本字符串
    is_luajit = "LuaJIT" in lua_version_str # 检查版本字符串中是否包含"LuaJIT"
    
    # lupa 0.25+ 提供了更直接的方式
    lupa_impl = getattr(lupa, 'lua_implementation', '') # 获取lupa的lua_implementation属性,如果不存在则返回空字符串
    lupa_ver_info = getattr(lupa, 'lua_version_info', None) # 获取lupa的lua_version_info属性,如果不存在则返回None

    print(f"Lua _VERSION: {
                lua_version_str}") # 打印Lua版本信息
    if lupa_impl: # 如果lupa_impl存在
        print(f"lupa.lua_implementation: {
                lupa_impl}") # 打印lupa的实现信息
        is_luajit = is_luajit or "LuaJIT" in lupa_impl # 更新is_luajit标志
    if lupa_ver_info: # 如果lupa_ver_info存在
        print(f"lupa.lua_version_info: Major={
                lupa_ver_info.major}, Minor={
                lupa_ver_info.minor}, Release={
                lupa_ver_info.release}") # 打印lupa版本详细信息
        # LuaJIT 2.0.x 通常报告为 Lua 5.1
        # LuaJIT 2.1.x 通常报告为 Lua 5.1 (或可配置为 Lua 5.2 兼容模式)

    if is_luajit: # 如果是LuaJIT
        print("Lupa appears to be linked with LuaJIT.") # 打印Lupa已链接LuaJIT信息
        # 进一步检查FFI库是否可用
        can_require_ffi = lua.eval('pcall(function() require("ffi") end)') # 尝试加载ffi库
        if can_require_ffi: # 如果加载成功
            print("LuaJIT FFI library is available via require('ffi').") # 打印FFI库可用信息
        else:
            print("Warning: LuaJIT detected, but 'require("ffi")' failed. FFI might not be usable.") # 打印FFI库不可用警告
    else:
        print("Lupa is likely NOT linked with LuaJIT. FFI examples will not work.") # 打印Lupa未链接LuaJIT信息
        raise EnvironmentError("Lupa not linked with LuaJIT, FFI tests cannot proceed.") # 抛出环境错误

except Exception as e: # 捕获异常
    print(f"Error during LuaJIT check: {
                e}") # 打印错误信息
    print("Ensure LuaJIT development libraries were available when installing lupa.") # 提示确保LuaJIT开发库可用
    raise # 重新抛出异常

如果lupa没有链接到LuaJIT,后续的FFI示例将无法工作。你需要确保在安装lupa时,环境中能找到LuaJIT的头文件和库。

9.2 LuaJIT FFI 基础回顾

在深入lupa交互前,简要回顾FFI的核心用法:

加载FFI库:

local ffi = require("ffi")

定义C类型和函数声明 (ffi.cdef):
使用类似C的语法在字符串中声明C结构体、联合、枚举、类型别名以及外部函数原型。

ffi.cdef[[
    // C类型定义
    typedef struct { int x; double y; } MyPoint;
    typedef int (*my_c_callback_func)(int a, int b); // 函数指针

    // C函数声明 (来自某个动态库,如libc)
    int printf(const char *format, ...);
    double sin(double x);
    void qsort(void *base, size_t nel, size_t width, int (*compar)(const void *, const void *));
]]

加载C库/符号命名空间 (ffi.loadffi.C):

ffi.C: 代表一个默认的C符号命名空间,通常包含标准C库(libc)的函数。在某些系统上,可能需要显式加载。
ffi.load("library_name"): 加载指定的动态链接库(如mylib.somylib.dll),并返回一个代表该库符号的命名空间。

local C = ffi.C  -- 使用默认libc符号
-- local mylib = ffi.load("mylib") -- 加载自定义库

创建C数据 (ffi.new, ffi.typeof):

ffi.new("C_type_name" [, initializer]): 分配一个指定C类型的对象。例如,local p = ffi.new("MyPoint", {0, 3.14})
ffi.typeof("C_type_name"): 获取一个C类型的对象,可以用来创建该类型的数组或指针。例如,local arr = ffi.new("int[10]")

调用C函数:
直接像调用Lua函数一样调用通过ffi.Cffi.load得到的C函数代理。

C.printf("Hello from FFI! Sin(0.5) = %f
", C.sin(0.5))

操作C数据:

访问结构体成员:p.x = 10; local val = p.y
数组索引:arr[0] = 123
指针解引用:local deref_val = ptr[0] (对于T* ptr) 或 ptr.fieldname (对于struct T* ptr)。

回调Lua函数从C:
可以将Lua函数转换为C函数指针传递给需要回调的C函数(如qsort的比较函数)。

local function compare_ints_lua(a_ptr, b_ptr)
    local a = ffi.cast("int*", a_ptr)[0] -- 将 void* 转为 int* 并解引用
    local b = ffi.cast("int*", b_ptr)[0]
    if a < b then return -1 elseif a > b then return 1 else return 0 end
end
-- 将Lua函数转换为C函数指针 (LuaJIT自动处理转换)
-- C.qsort(int_array, num_elements, ffi.sizeof("int"), compare_ints_lua)

9.3 Python 通过 lupa 与 LuaJIT FFI 交互

Python本身不能直接使用LuaJIT FFI。交互总是通过lupa执行Lua脚本,而Lua脚本内部使用FFI。Python可以:

触发执行使用FFI的Lua代码
向Lua传递数据,这些数据可能被FFI用于C调用。
从Lua接收由FFI产生或处理过的数据

9.3.1 示例1:调用标准C库函数 (如 printf, gettimeofday)

Lua脚本 (ffi_libc_calls.lua):

-- ffi_libc_calls.lua
local ffi = require("ffi")

-- 声明需要的C函数和结构体
ffi.cdef[[
    // printf from stdio.h
    int printf(const char *format, ...);

    // gettimeofday from sys/time.h (POSIX)
    // (Windows上没有,需要用 GetSystemTimeAsFileTime 或其他)
    struct timeval {
        long tv_sec;  // seconds
        long tv_usec; // microseconds
    };
    struct timezone { // 通常不使用,可以设为NULL
        int tz_minuteswest;
        int tz_dsttime;
    };
    int gettimeofday(struct timeval *tv, struct timezone *tz);

    // For Windows, an alternative (simplified)
    // typedef unsigned long long FILETIME;
    // void GetSystemTimeAsFileTime(FILETIME* lpSystemTimeAsFileTime);
]]

-- 获取C库的命名空间
local C_lib
if ffi.os == "Windows" then -- 如果是Windows系统
    -- 对于Windows,printf在msvcrt.dll中
    -- GetSystemTimeAsFileTime 在 kernel32.dll 中
    -- 为了简单,我们只演示printf,假设msvcrt能被默认找到或通过 ffi.load("msvcrt")
    C_lib = ffi.C -- 或者 ffi.load("msvcrt")
    -- Windows下 gettimeofday 不可用,下面的调用会失败或需要包装
else -- POSIX 系统
    C_lib = ffi.C -- 默认的libc
end

function lua_ffi_printf(message_str)
    if C_lib.printf then -- 检查printf函数是否存在
        return C_lib.printf("[Lua via FFI printf] %s
", message_str) -- 调用C的printf函数
    else
        print("[Lua] C_lib.printf not available on this system via ffi.C") -- 打印printf不可用信息
        return -1 -- 返回-1表示失败
    end
end

function lua_ffi_get_current_time_us()
    if ffi.os == "Windows" then -- 如果是Windows系统
        -- -- Windows FFI GetSystemTimeAsFileTime 示例 (较复杂,这里省略完整转换)
        -- local ft = ffi.new("FILETIME")
        -- C_lib.GetSystemTimeAsFileTime(ft)
        -- -- FILETIME 是100纳秒间隔,需要转换
        -- return tonumber(ft) -- 这只是一个占位符,不是真实的微秒数
        print("[Lua] gettimeofday not available on Windows. Returning placeholder.") -- 打印gettimeofday不可用信息
        return -1, 0 -- 返回-1, 0表示失败
    else -- POSIX 系统
        if not C_lib.gettimeofday then -- 如果gettimeofday函数不存在
            print("[Lua] C_lib.gettimeofday not available.") -- 打印gettimeofday不可用信息
            return -1, 0 -- 返回-1, 0表示失败
        end
        local tv = ffi.new("struct timeval") -- 创建timeval结构体实例
        local tz = nil -- timezone通常设为NULL ffi.new("struct timezone")
        
        local ret = C_lib.gettimeofday(tv, tz) -- 调用C的gettimeofday函数
        if ret == 0 then -- 如果调用成功
            return tv.tv_sec, tv.tv_usec -- 返回秒和微秒
        else
            -- 可以尝试获取 errno ffi.errno()
            print("[Lua] gettimeofday call failed. Errno:", ffi.errno()) -- 打印gettimeofday调用失败信息
            return -1, ret -- 返回-1和错误码
        end
    end
end

-- 返回一个包含这些函数的table,方便Python调用
return {
            
    call_printf = lua_ffi_printf,
    get_time_us = lua_ffi_get_current_time_us
}

Python调用代码 (run_ffi_libc.py):

import lupa
import os
import time

def execute_ffi_libc_example(): # 定义执行FFI libc示例的函数
    try:
        lua = lupa.LuaRuntime(unpack_returned_tuples=False) # 创建Lua运行时实例,禁用元组自动解包
        # 检查是否为LuaJIT
        if "LuaJIT" not in lua.globals()._VERSION and ("LuaJIT" not in getattr(lupa, 'lua_implementation', '')): # 如果不是LuaJIT
            print("This example requires Lupa to be linked with LuaJIT.") # 打印需要链接LuaJIT的提示
            return # 返回
        if not lua.eval('pcall(function() require("ffi") end)'): # 如果无法加载ffi库
            print("LuaJIT FFI library 'ffi' could not be required.") # 打印无法加载ffi库的提示
            return # 返回
        print("Lupa with LuaJIT and FFI detected.") # 打印检测成功信息
    except Exception as e:
        print(f"Failed to initialize LuaJIT runtime for FFI: {
              e}") # 打印初始化失败信息
        return # 返回

    script_path = os.path.join(os.path.dirname(__file__), "ffi_libc_calls.lua") # 获取Lua脚本路径
    if not os.path.exists(script_path): # 如果脚本路径不存在
        # 创建一个示例 ffi_libc_calls.lua 文件以便运行
        example_ffi_lua_content = """
local ffi = require("ffi")
ffi.cdef[[ int printf(const char *format, ...); ]]
local C = ffi.C
function lua_ffi_printf(msg) return C.printf("[LuaFFI] %s\n", msg) end
function lua_ffi_get_current_time_us() if ffi.os == "Windows" then return -1,0 else ffi.cdef[[ struct timeval{long tv_sec; long tv_usec;}; int gettimeofday(struct timeval *tv, void *tz);]] local tv=ffi.new("struct timeval"); C.gettimeofday(tv,nil); return tv.tv_sec, tv.tv_usec; end end
return { call_printf = lua_ffi_printf, get_time_us = lua_ffi_get_current_time_us }
        """ # 示例FFI Lua脚本内容
        with open(script_path, "w", encoding="utf-8") as f: # 打开示例Lua文件
            f.write(example_ffi_lua_content) # 写入示例内容
        print(f"Created example FFI script at {
              script_path}") # 打印创建信息


    try:
        lua_ffi_module_proxy = lua.execute(f'return dofile("{
              script_path}")') # 执行Lua脚本并获取返回的模块代理
        if not lua_ffi_module_proxy: # 如果模块代理不存在
            print(f"Failed to load or execute Lua FFI script: {
              script_path}") # 打印加载或执行失败信息
            return # 返回

        # 1. 调用 lua_ffi_printf
        message_to_print = f"Hello from Python at {
              time.time()}" # 要打印的消息
        print(f"
[Python] Calling Lua's lua_ffi_printf with: '{
              message_to_print}'") # 打印调用信息
        # lua_ffi_module_proxy.call_printf 是一个 LuaFunctionProxy
        # 它返回一个元组 (printf的返回值)
        printf_ret_tuple = lua_ffi_module_proxy.call_printf(message_to_print) # 调用Lua的printf函数
        if printf_ret_tuple: # 如果返回元组存在
            print(f"[Python] lua_ffi_printf (C's printf) returned: {
              printf_ret_tuple[0]}") # 打印C的printf返回值
        else:
            print("[Python] lua_ffi_printf did not return as expected.") # 打印未按预期返回信息

        # 2. 调用 lua_ffi_get_current_time_us
        print("
[Python] Calling Lua's lua_ffi_get_current_time_us...") # 打印调用信息
        time_ret_tuple = lua_ffi_module_proxy.get_time_us() # 调用Lua的get_time_us函数

        if time_ret_tuple and len(time_ret_tuple) == 2: # 如果返回元组存在且长度为2
            sec_lua, usec_lua = time_ret_tuple # 解包秒和微秒
            if sec_lua != -1: # 如果秒不等于-1 (表示成功)
                print(f"[Python] Time from Lua FFI (gettimeofday): Seconds={
              sec_lua}, Microseconds={
              usec_lua}") # 打印从Lua FFI获取的时间
                timestamp_from_lua = float(sec_lua) + float(usec_lua) / 1_000_000.0 # 计算时间戳
                print(f"[Python] Calculated timestamp from Lua: {
              timestamp_from_lua:.6f}") # 打印计算出的时间戳
                
                # 与Python的time.time()比较 (会有少许误差)
                py_time_now = time.time() # 获取Python当前时间
                print(f"[Python] time.time() for comparison:  {
              py_time_now:.6f}") # 打印Python时间以供比较
                print(f"[Python] Difference: {
              abs(py_time_now - timestamp_from_lua):.6f} seconds") # 打印时间差
            else:
                print(f"[Python] Lua's get_time_us indicated failure or OS incompatibility (e.g. Windows). Return code: {
              usec_lua}") # 打印失败或不兼容信息
        else:
            print(f"[Python] lua_ffi_get_current_time_us did not return as expected. Received: {
              time_ret_tuple}") # 打印未按预期返回信息

    except lupa.LuaError as e: # 捕获Lua错误
        print(f"[Python] LuaError during FFI example: {
              e}") # 打印Lua错误信息
    except FileNotFoundError as e: # 捕获文件未找到错误
        print(f"[Python] FFI Lua script not found: {
              e}") # 打印文件未找到信息
    except EnvironmentError as e: # 捕获环境错误
        print(f"[Python] EnvironmentError (likely Lupa not linked with LuaJIT): {
              e}") # 打印环境错误信息
    except Exception as e: # 捕获其他Python错误
        print(f"[Python] An unexpected Python error occurred: {
              e}") # 打印未预期Python错误信息
        import traceback # 导入traceback模块
        # traceback.print_exc() # 打印完整的堆栈跟踪

if __name__ == "__main__": # 如果作为主模块运行
    execute_ffi_libc_example() # 执行FFI libc示例

代码解释:

ffi_libc_calls.lua:

加载ffi库。
使用ffi.cdef声明了C标准库函数printf和POSIX函数gettimeofday(以及相关的struct timeval)。它还包含了Windows下gettimeofday不可用的条件处理。
lua_ffi_printf直接调用C_lib.printf
lua_ffi_get_current_time_us调用C_lib.gettimeofday,分配struct timeval,并返回秒和微秒。它也处理了Windows上此函数不可用的情况。
最后,它返回一个包含这两个Lua函数代理的table,方便Python按名称调用。

run_ffi_libc.py:

首先进行严格的LuaJIT和FFI可用性检查。
lua.execute(f'return dofile("{script_path}")'): 执行Lua脚本。由于脚本最后返回一个table,这个table的代理被赋给lua_ffi_module_proxy
lua_ffi_module_proxy.call_printf(...)lua_ffi_module_proxy.get_time_us(): Python通过lupa的函数代理调用Lua中定义的函数。
参数和返回值都由lupa自动在Python和Lua(以及Lua FFI与C)之间转换。
由于LuaRuntime(unpack_returned_tuples=False)get_time_us()返回的是一个包含两个元素(秒和微秒)的Python元组。

9.3.2 示例2:使用FFI操作自定义C库

假设你有一个用C编写的自定义动态库(例如 libmymath.somymath.dll),它提供了一些高性能的数学运算。

C库代码 (mymath.c) – 编译为共享库:

// mymath.c
// Compile (Linux): gcc -shared -fPIC -o libmymath.so mymath.c
// Compile (Windows): gcc -shared -o mymath.dll mymath.c (using MinGW)
// Or use MSVC: cl /LD mymath.c /link /out:mymath.dll

#ifdef _WIN32
    #define DLLEXPORT __declspec(dllexport) // Windows动态库导出宏
#else
    #define DLLEXPORT // Linux/macOS不需要特殊宏
#endif

#include <stdio.h> // 包含标准输入输出头文件

DLLEXPORT double add_doubles(double a, double b) {
             // 定义导出函数:加法
    // printf("[C libmymath] add_doubles(%.2f, %.2f) called.
", a, b); // 打印调用信息 (调试用)
    return a + b; // 返回a和b的和
}

typedef struct {
             // 定义结构体Vector2D
    double x; // x坐标
    double y; // y坐标
} Vector2D;

DLLEXPORT void scale_vector(Vector2D* vec, double factor) {
             // 定义导出函数:缩放向量
    if (vec) {
             // 如果向量指针不为空
        // printf("[C libmymath] scale_vector(vec->x=%.2f, vec->y=%.2f, factor=%.2f) called.
", vec->x, vec->y, factor); // 打印调用信息 (调试用)
        vec->x *= factor; // 缩放x坐标
        vec->y *= factor; // 缩放y坐标
    }
}

Lua脚本 (ffi_custom_lib.lua):

-- ffi_custom_lib.lua
local ffi = require("ffi")

-- 确定库的路径和名称 (根据操作系统调整)
local lib_name -- 初始化库名变量
if ffi.os == "Windows" then -- 如果是Windows系统
    lib_name = "mymath.dll" -- Windows库名
elseif ffi.os == "OSX" then -- 如果是macOS系统
    lib_name = "./libmymath.dylib" -- macOS库名 (可能需要DYLD_LIBRARY_PATH或绝对路径)
else -- Linux等
    lib_name = "./libmymath.so" -- Linux库名 (当前目录,或者系统库路径)
end
-- 重要: 确保编译好的共享库与此脚本在同一目录,或在系统的库搜索路径中。
-- 或者提供绝对路径给 ffi.load()。

-- 尝试加载自定义库
local MyMathLib, load_err = pcall(ffi.load, lib_name) -- 安全加载库
if not MyMathLib then -- 如果加载失败
    print(string.format("[Lua FFI] Error loading custom library '%s': %s", lib_name, tostring(load_err))) -- 打印加载错误信息
    print("[Lua FFI] Make sure the library is compiled and in the correct path.") -- 提示检查库路径
    -- 返回一个空的table或者错误指示,让Python知道失败了
    return {
             error = "Failed to load custom C library: " .. lib_name .. " - " .. tostring(load_err) } -- 返回错误信息
end
print(string.format("[Lua FFI] Successfully loaded custom library: %s", lib_name)) -- 打印加载成功信息

-- 定义C库中的函数和结构体
ffi.cdef[[
    // Functions from mymath library
    double add_doubles(double a, double b);

    typedef struct {
        double x;
        double y;
    } Vector2D; // 与C中定义一致

    void scale_vector(Vector2D* vec, double factor);
]]

-- Lua包装函数
function lua_add(a_num, b_num)
    return MyMathLib.add_doubles(a_num, b_num) -- 调用C库的add_doubles函数
end

function lua_create_and_scale_vector(x_val, y_val, scale_factor_val)
    -- 使用ffi.new创建C结构体实例
    -- 初始化器 {x_val, y_val} 对应结构体成员顺序
    local vec_cdata = ffi.new("Vector2D", {
            x_val, y_val}) -- 创建Vector2D结构体实例
    print(string.format("[Lua FFI] Created Vector2D: x=%.2f, y=%.2f", vec_cdata.x, vec_cdata.y)) -- 打印创建的向量信息

    -- 调用C函数,传递C数据结构的指针
    MyMathLib.scale_vector(vec_cdata, scale_factor_val) -- 调用C库的scale_vector函数
    print(string.format("[Lua FFI] Scaled Vector2D: x=%.2f, y=%.2f", vec_cdata.x, vec_cdata.y)) -- 打印缩放后的向量信息

    -- 将C数据转换回Lua table返回给Python (如果需要)
    return {
             x = vec_cdata.x, y = vec_cdata.y } -- 返回包含缩放后坐标的Lua table
end

-- 返回一个包含这些函数的table
return {
            
    my_add = lua_add,
    create_scale_vec = lua_create_and_scale_vector,
    is_lib_loaded = (MyMathLib ~= nil) -- 添加一个标志表示库是否加载成功
}

Python调用代码 (run_ffi_custom.py):

import lupa
import os
import ctypes # 用于辅助查找库的路径 (可选)

def find_lib_path(lib_name_base): # 定义查找库路径的函数
    """辅助函数,尝试定位共享库,考虑不同平台的扩展名。"""
    # 尝试的扩展名顺序和前缀
    if os.name == 'nt': # Windows
        extensions = ['.dll'] # Windows扩展名
        prefixes = [''] # Windows前缀
    elif sys.platform == 'darwin': # macOS
        extensions = ['.dylib', '.so'] # macOS扩展名
        prefixes = ['', 'lib'] # macOS前缀
    else: # Linux and other POSIX
        extensions = ['.so'] # Linux扩展名
        prefixes = ['', 'lib'] # Linux前缀
    
    # 检查的目录:当前目录,脚本所在目录
    check_dirs = [os.getcwd(), os.path.dirname(os.path.abspath(__file__))] # 检查目录列表
    # print(f"Searching for {lib_name_base} in {check_dirs}")

    for directory in check_dirs: # 遍历检查目录
        for prefix in prefixes: # 遍历前缀
            for ext in extensions: # 遍历扩展名
                full_name = prefix + lib_name_base + ext # 构造完整库名
                path = os.path.join(directory, full_name) # 构造完整路径
                if os.path.exists(path): # 如果路径存在
                    # print(f"Found library at: {path}")
                    return path # 返回路径
    # print(f"Library {lib_name_base} not found in typical local paths.")
    return lib_name_base # 如果没找到,返回基础名,让系统加载器尝试

def ensure_custom_lib_exists(): # 定义确保自定义库存在的函数
    """
    检查或尝试编译示例C库。
    这只是为了让示例能独立运行,实际项目中C库会预先编译好。
    """
    base_name = "mymath" # 基础库名
    c_file = os.path.join(os.path.dirname(__file__), f"{
              base_name}.c") # C文件名
    
    # 确定目标库文件名
    if os.name == 'nt': # Windows
        lib_file = os.path.join(os.path.dirname(__file__), f"{
              base_name}.dll") # Windows库文件名
        compile_cmd = ["gcc", "-shared", "-o", lib_file, c_file] # Windows编译命令
    elif sys.platform == 'darwin': # macOS
        lib_file = os.path.join(os.path.dirname(__file__), f"lib{
              base_name}.dylib") # macOS库文件名
        compile_cmd = ["gcc", "-shared", "-fPIC", "-o", lib_file, c_file] # macOS编译命令
    else: # Linux
        lib_file = os.path.join(os.path.dirname(__file__), f"lib{
              base_name}.so") # Linux库文件名
        compile_cmd = ["gcc", "-shared", "-fPIC", "-o", lib_file, c_file] # Linux编译命令

    if os.path.exists(lib_file): # 如果库文件已存在
        print(f"Custom C library '{
              lib_file}' already exists.") # 打印库已存在信息
        return True # 返回True

    # 尝试创建示例C文件 (如果不存在)
    if not os.path.exists(c_file): # 如果C文件不存在
        c_content = """
#ifdef _WIN32
    #define DLLEXPORT __declspec(dllexport)
#else
    #define DLLEXPORT
#endif
#include <stdio.h>
DLLEXPORT double add_doubles(double a, double b) { return a + b; }
typedef struct { double x; double y; } Vector2D;
DLLEXPORT void scale_vector(Vector2D* vec, double factor) { if (vec) { vec->x *= factor; vec->y *= factor; } }
        """ # 示例C代码内容
        with open(c_file, "w", encoding="utf-8") as f: # 打开C文件
            f.write(c_content) # 写入C代码内容
        print(f"Created example C source file: {
              c_file}") # 打印创建C文件信息

    print(f"Attempting to compile C library: {
              ' '.join(compile_cmd)}") # 打印编译命令信息
    try:
        import subprocess # 导入subprocess模块
        process = subprocess.run(compile_cmd, check=True, capture_output=True, text=True) # 执行编译命令
        print(f"C library compiled successfully: {
              lib_file}") # 打印编译成功信息
        # print("Compiler STDOUT:", process.stdout) # 打印编译器标准输出 (调试用)
        # print("Compiler STDERR:", process.stderr) # 打印编译器标准错误 (调试用)
        return True # 返回True
    except FileNotFoundError: # 捕获文件未找到错误 (通常是gcc未安装或不在PATH)
        print("Error: GCC (C compiler) not found. Please compile mymath.c manually.") # 打印GCC未找到信息
    except subprocess.CalledProcessError as e: # 捕获子进程调用错误
        print(f"Error compiling C library:") # 打印编译错误信息
        print("Command:", ' '.join(e.cmd)) # 打印命令
        print("Return Code:", e.returncode) # 打印返回码
        print("STDOUT:", e.stdout) # 打印标准输出
        print("STDERR:", e.stderr) # 打印标准错误
    return False # 返回False


def execute_ffi_custom_lib_example(): # 定义执行FFI自定义库示例的函数
    # 确保C库已编译 (对于这个自动化示例)
    if not ensure_custom_lib_exists(): # 如果自定义库不存在或编译失败
        print("Failed to ensure custom C library exists. Aborting FFI custom lib example.") # 打印失败信息
        return # 返回

    try:
        lua = lupa.LuaRuntime(unpack_returned_tuples=False) # 创建Lua运行时实例
        if "LuaJIT" not in lua.globals()._VERSION and ("LuaJIT" not in getattr(lupa, 'lua_implementation', '')): # 如果不是LuaJIT
            print("This example requires Lupa to be linked with LuaJIT.") # 打印需要链接LuaJIT的提示
            return # 返回
        if not lua.eval('pcall(function() require("ffi") end)'): # 如果无法加载ffi库
            print("LuaJIT FFI library 'ffi' could not be required.") # 打印无法加载ffi库的提示
            return # 返回
    except Exception as e:
        print(f"Failed to initialize LuaJIT runtime for FFI: {
              e}") # 打印初始化失败信息
        return # 返回

    script_path = os.path.join(os.path.dirname(__file__), "ffi_custom_lib.lua") # 获取Lua脚本路径
    if not os.path.exists(script_path): # 如果脚本路径不存在
        example_ffi_custom_lua_content = """
local ffi = require("ffi")
local lib_name = ffi.os == "Windows" and "mymath.dll" or (ffi.os == "OSX" and "./libmymath.dylib" or "./libmymath.so")
local MyMathLib, err = pcall(ffi.load, lib_name)
if not MyMathLib then return { error = "Failed to load: " .. tostring(err), is_lib_loaded=false } end
ffi.cdef[[ double add_doubles(double a, double b); typedef struct {double x; double y;} Vector2D; void scale_vector(Vector2D* vec, double factor); ]]
return {
    is_lib_loaded = true,
    my_add = function(a,b) return MyMathLib.add_doubles(a,b) end,
    create_scale_vec = function(x,y,f) local v=ffi.new("Vector2D",{x,y}); MyMathLib.scale_vector(v,f); return {x=v.x, y=v.y} end
}
        """ # 示例FFI自定义Lua脚本内容
        with open(script_path, "w", encoding="utf-8") as f: # 打开示例Lua文件
            f.write(example_ffi_custom_lua_content) # 写入示例内容
        print(f"Created example FFI custom lib script at {
              script_path}") # 打印创建信息

    try:
        # 执行Lua脚本,它会加载C库并返回函数代理
        lua_custom_math_proxy = lua.execute(f'return dofile("{
              script_path}")') # 执行Lua脚本并获取返回的代理

        if lua_custom_math_proxy.error then # 如果加载C库时出错
            print(f"[Python] Error from Lua script during C library loading: {
              lua_custom_math_proxy.error}") # 打印加载错误信息
            return # 返回
        if not lua_custom_math_proxy.is_lib_loaded: # 如果库未加载成功
             print(f"[Python] Lua script indicated C library was not loaded.") # 打印库未加载信息
             return # 返回

        print("[Python] Lua FFI script for custom C library executed successfully.") # 打印执行成功信息

        # 1. 调用 my_add (即C的 add_doubles)
        num1, num2 = 10.5, 22.3 # 定义要相加的数字
        print(f"
[Python] Calling Lua's my_add({
              num1}, {
              num2}) which calls C's add_doubles...") # 打印调用信息
        add_result_tuple = lua_custom_math_proxy.my_add(num1, num2) # 调用Lua的my_add函数
        # `my_add`直接返回C函数结果,如果C函数返回单个值且unpack_returned_tuples=False,
        # Lupa仍会将其包装在元组中,除非lupa对此有特定优化。
        # 对于返回单个double的C函数,lupa通常会直接返回该double值 (如果Lua侧没有多余的return包装)。
        # 假设 `add_doubles` 返回的 double 被 lupa 正确转换为 Python float
        # 在 Lua 脚本中 `return MyMathLib.add_doubles(a,b)`,lupa 会得到单个返回值
        # 即使 unpack_returned_tuples=False,单个返回值通常不会被额外包装成元组,除非 lupa 版本行为有变。
        # 为了安全,我们检查类型。
        if isinstance(add_result_tuple, tuple) and len(add_result_tuple) == 1: # 如果返回是单元素元组
            add_result = add_result_tuple[0] # 获取元组的第一个元素
        elif isinstance(add_result_tuple, (float, int)): # 如果返回是浮点数或整数
            add_result = add_result_tuple #直接使用
        else:
            print(f"[Python] my_add returned an unexpected type or structure: {
              add_result_tuple}") # 打印未预期类型信息
            add_result = None # 将结果设为None

        if add_result is not None: # 如果结果不为None
            print(f"[Python] Result from C's add_doubles via Lua FFI: {
              add_result} (Expected: {
              num1 + num2})") # 打印结果信息

        # 2. 调用 create_scale_vec (创建C struct, 调用C函数修改, 返回Lua table)
        vx, vy, factor = 2.0, 3.0, 1.5 # 定义向量坐标和缩放因子
        print(f"
[Python] Calling Lua's create_scale_vec({
              vx}, {
              vy}, {
              factor})...") # 打印调用信息
        # Lua的 create_scale_vec 返回一个Lua table {x=..., y=...}
        # lupa 会将其转换为一个 LuaTableProxy
        scaled_vec_proxy = lua_custom_math_proxy.create_scale_vec(vx, vy, factor) # 调用Lua的create_scale_vec函数

        if scaled_vec_proxy and hasattr(scaled_vec_proxy, 'items'): # 如果代理存在且可迭代
            # 将LuaTableProxy转换为Python dict以便打印
            scaled_vec_py = {
            k: v for k, v in scaled_vec_proxy.items()} # 将代理转换为Python字典
            print(f"[Python] Scaled vector from Lua (originally C struct): {
              scaled_vec_py}") # 打印缩放后的向量信息
            print(f"  Expected x: {
              vx * factor}, Expected y: {
              vy * factor}") # 打印预期结果
        else:
            print(f"[Python] create_scale_vec did not return a table-like proxy: {
              scaled_vec_proxy}") # 打印未返回table代理信息


    except lupa.LuaError as e: # 捕获Lua错误
        print(f"[Python] LuaError during FFI custom library example: {
              e}") # 打印Lua错误信息
    except FileNotFoundError as e: # 捕获文件未找到错误
        print(f"[Python] FFI Lua script for custom lib not found: {
              e}") # 打印文件未找到信息
    except EnvironmentError as e: # 捕获环境错误
        print(f"[Python] EnvironmentError (Lupa/LuaJIT/FFI issue): {
              e}") # 打印环境错误信息
    except Exception as e: # 捕获其他Python错误
        print(f"[Python] An unexpected Python error occurred in FFI custom lib example: {
              e}") # 打印未预期Python错误信息
        import traceback # 导入traceback模块
        # traceback.print_exc() # 打印完整的堆栈跟踪

if __name__ == "__main__": # 如果作为主模块运行
    execute_ffi_custom_lib_example() # 执行FFI自定义库示例

代码解释:

mymath.c: 一个简单的C源文件,定义了add_doubles函数和一个Vector2D结构体以及scale_vector函数(通过指针修改结构体)。它被编译成一个共享库。
ffi_custom_lib.lua:

通过ffi.load(lib_name)加载编译好的自定义C库 (libmymath.somymath.dll)。这里包含了对不同操作系统库名的基本处理和错误检查。
使用ffi.cdef声明从C库中导入的函数add_doubles, scale_vector以及结构体Vector2D
lua_add直接调用MyMathLib.add_doubles
lua_create_and_scale_vector:

使用ffi.new("Vector2D", {x_val, y_val})在Lua中创建了一个C的Vector2D结构体实例 (vec_cdata)。
将这个vec_cdata(LuaJIT FFI 会自动处理指针传递)传递给MyMathLib.scale_vector,C函数会直接修改这个内存中的数据。
最后,它从vec_cdata中读取修改后的xy值,并将它们包装在一个新的Lua table中返回给Python。

run_ffi_custom.py:

ensure_custom_lib_exists(): 这是一个辅助函数,尝试自动编译示例的mymath.c文件,以确保共享库存在,使得示例更容易独立运行。在实际项目中,C库会由构建系统预先编译好。
Python代码执行ffi_custom_lib.lua脚本,获取其返回的包含Lua函数代理的table。
调用lua_custom_math_proxy.my_add,它会间接触发C函数add_doubles
调用lua_custom_math_proxy.create_scale_vec。Python将普通的数字传递给Lua,Lua内部使用FFI创建C结构体,调用C函数修改它,然后将结果(作为Lua table)返回给Python。lupa将这个Lua table转换为LuaTableProxy

9.3.3 FFI的优势与lupa结合点

性能: 对于计算密集型任务,用C实现并通过FFI从Lua调用,通常比纯Lua或Python实现快得多。LuaJIT的JIT编译器在调用FFI函数时开销非常低。
直接内存访问: FFI允许Lua直接操作C数据结构,避免了在Lua和C之间进行数据复制或转换的开销(相比传统的lua_push/pop绑定方式)。
复用现有C库: 可以轻松地将现有的C/C++库集成到Lua脚本环境中,进而供Python应用使用。
Python的角色:

Python仍然作为主应用或编排层。
Python负责配置和管理LuaJIT环境(通过lupa)。
Python准备输入数据给Lua,并处理Lua(可能通过FFI与C交互后)返回的结果。
Python不直接与FFI交互,而是通过lupa执行那些使用FFI的Lua代码。

9.3.4 注意事项和挑战

lupacdata:

当Lua FFI函数返回一个cdata对象(如ffi.new创建的C结构体实例或指针)给Python时,lupa如何处理它?

lupa可能将cdata视为一种特殊的userdata。
直接在Python中操作这个cdata代理可能功能有限,除非lupa有特定的代码来理解和交互FFI的cdata类型。
最佳实践: 通常是在Lua侧完成所有对cdata的操作,然后将结果转换为Lua的基本类型或table再返回给Python。如lua_create_and_scale_vector示例中,最后返回的是包含xy的普通Lua table,而不是Vector2Dcdata本身。
如果确实需要将cdata指针(例如,一个句柄)传递回Python以便后续再传递给另一个FFI调用,可以考虑将其转换为Lua number(如果指针能安全地表示为数字)或一个轻量级userdata。

内存管理:

通过ffi.new在Lua中分配的C数据,其生命周期由LuaJIT的GC管理(当cdata对象不再被Lua引用时)。
如果C库内部自己分配了内存并返回指针给Lua FFI,那么Lua/FFI侧需要知道何时以及如何释放这些内存(例如,C库提供一个free_my_data(void*)函数,Lua FFI在适当的时候调用它,可能通过ffi.gc()cdata设置终结器)。Python通过lupa间接依赖于Lua侧正确的内存管理。

指针和所有权: 当在Python、Lua FFI和C代码之间传递数据(尤其是通过指针)时,必须非常清晰地定义数据的所有权和生命周期,以避免悬空指针或重复释放。

错误处理: C库函数通常通过返回值或设置errno来指示错误。Lua FFI脚本需要检查这些错误,并通过error()或返回特定的错误码/消息给Python。Python则通过捕获lupa.LuaError来处理。

ABI兼容性: FFI依赖于C的ABI(Application Binary Interface)。确保LuaJIT FFI声明的C类型和函数签名与实际编译的C库的ABI兼容至关重要。

复杂类型和回调:

处理复杂的C结构体、联合、位域等需要仔细的ffi.cdef声明。
将Lua函数作为回调传递给C函数(如qsort的比较函数)是FFI的强大功能。Python可以通过lupa执行调用这类C函数的Lua代码。Python函数本身不能直接作为FFI回调传递给C(除非lupa提供了特殊的机制来创建这样的C函数指针代理,但这会更复杂)。通常是Lua函数作为直接的FFI回调。

构建和部署: 使用FFI引入了对外部C库的依赖。你需要确保这些C库在目标系统上可用,并且LuaJIT(通过lupa)能够找到并加载它们。库的路径可能需要配置。

9.4 何时选择 LuaJIT FFI 与 lupa 结合?

性能瓶颈: 当Python或标准Lua的性能无法满足应用的某些关键部分(如数值计算、信号处理、图像处理的底层操作)时。
集成现有C/C++资产: 如果有大量现成的、高性能的C/C++库需要被Python应用利用,通过LuaJIT FFI包装它们可能比为Python编写复杂的C扩展(如Cython或ctypes,尽管ctypes也是一种FFI)更快速或更方便,尤其是如果团队中有人熟悉Lua。
需要动态调用C函数: 如果需要根据配置或运行时条件动态选择和调用不同的C函数或库,Lua的脚本特性结合FFI提供了很大的灵活性。
希望Lua脚本有更强的底层能力: 允许Lua脚本直接进行底层系统调用或与硬件相关的操作(需谨慎使用)。

第十章:lupa 性能考量与优化策略

虽然lupa为Python和Lua之间的集成提供了极大的便利性和灵活性,但在性能敏感的应用中,理解其调用开销并采取适当的优化措施至关重要。本章将剖析主要的性能影响因素,并提供优化建议。

10.1 lupa 交互的主要开销来源

跨语言函数调用开销 (Call Overhead):

Python调用Lua函数:

lupa需要将Python参数转换为Lua类型并压入Lua栈。
查找并压入目标Lua函数。
执行lua_pcall。这涉及到Lua解释器的函数调用机制。
获取Lua函数的返回值(可能多个),从Lua栈弹出并转换为Python类型。
错误检查和转换。

Lua调用Python函数 (回调):

Lua调用lupa创建的C代理函数。
C代理函数从Lua栈获取参数,并将其转换为Python类型。
通过Python C API查找并调用目标Python函数(可能涉及PyObject_Call)。
获取Python函数的返回值,并将其转换为Lua类型压入Lua栈。
Python异常到Lua错误的转换。

这个过程涉及多次类型检查、数据封送/解封 (marshalling/unmarshalling)、栈操作以及两种不同语言虚拟机之间的切换,即使对于非常简单的函数调用,其固定开销也远高于同一种语言内部的函数调用。

数据类型转换开销 (Data Conversion Overhead):

Python到Lua:

基本类型(数字、布尔、nil/None、短字符串)转换相对较快。
复杂类型如Python list/tuple/dict转换为Lua table时,lupa需要遍历Python集合,为每个元素/键值对递归调用转换逻辑,并在Lua中创建新的table和元素。对于大型或深层嵌套的集合,这个开销可能很大。
Python对象作为userdata传递给Lua,创建userdata和元表本身有开销,但后续通过元方法访问通常是按需的。

Lua到Python:

基本类型转换快。
Lua table 默认通过LuaTableProxy传递给Python。代理本身的创建开销不大。但当Python代码实际迭代或访问代理的元素时,每次访问都会触发一次从Lua到Python的转换(对于被访问的元素)。如果需要将整个Lua table完整转换为Python dict/list,则开销与Python到Lua的转换类似。
cdata (LuaJIT FFI) 如果需要转换为Python可直接理解的结构,也可能涉及开销。

代理对象开销 (Proxy Object Overhead):

当Lua table、function、thread或(某些类型的)userdata传递给Python时,lupa会创建Python代理对象(如LuaTableProxy, LuaFunctionProxy)。
这些代理对象的创建、方法调用(内部会再次进入Lua执行相应操作)以及生命周期管理(通过Lua注册表锚定Lua对象)都有一定的开销。
对代理对象的频繁、细粒度操作(例如,在Python循环中逐个访问大型Lua table代理的元素)会累积显著的调用和转换开销。

LuaJIT FFI 调用开销 (与纯C相比):

虽然LuaJIT FFI的调用开销极低(接近直接C调用),但如果FFI调用本身非常简短且被极其频繁地从Lua中调用,其累积的微小开销也可能变得明显。
通过lupa间接使用FFI时,主要的开销还是在Python到Lua的调用环节,而不是Lua到C的FFI调用环节。

Lua解释器/JIT编译器本身:

标准Lua是解释执行的,虽然很快,但不如编译型语言。
LuaJIT通过JIT编译可以大幅提升Lua代码的执行速度,但JIT编译本身需要时间(“预热”阶段)。对于只执行一次或几次的短脚本,可能无法从JIT中充分受益。某些Lua代码模式可能不利于JIT编译(“JIT毒药”)。

垃圾回收 (GC) 协调:

lupa需要管理Python对象在Lua中的生命周期(通过userdata的__gc元方法和Python引用计数)以及Lua对象在Python中的生命周期(通过代理对象的析构和Lua注册表引用)。
虽然lupa努力正确处理,但GC活动本身(尤其是在两种语言都有大量临时对象被创建和销毁时)可能会引入间歇性的性能停顿。跨语言的循环引用如果未能被打破,会导致内存泄漏,间接影响性能。

错误处理机制:

在跨语言调用中,异常/错误的捕获、转换和重新抛出也是有开销的。虽然对于正常执行路径影响不大,但在错误频发的情况下会成为负担。

10.2 性能分析与基准测试

在进行优化之前,首先需要确定性能瓶颈在哪里。

Python Profilers: 使用Python内置的cProfileprofile模块,以及可视化工具如snakevizpyinstrument,来分析Python代码的执行时间,找出耗时较多的lupa调用或数据处理部分。

import cProfile
import pstats
import lupa
# from io import StringIO # Python 3
# import आपका_lupa_कोड_मॉड्यूल # 假设你的lupa交互代码在一个模块中

# def function_using_lupa():
#     lua = lupa.LuaRuntime()
#     # ... 大量的lupa调用和数据处理 ...
#     for _ in range(10000):
#         lua.eval("math.sin(0.5)") # 执行简单的Lua代码
#     py_list = list(range(1000)) # 创建一个Python列表
#     lua_table = lua.table_from(py_list) # 将Python列表转换为Lua table
#     for i in lua_table.values(): # 遍历Lua table的值
#         pass # 空操作

# if __name__ == "__main__":
#     profiler = cProfile.Profile() # 创建cProfile实例
#     profiler.enable() # 开始性能分析

#     function_using_lupa() # 执行包含lupa调用的函数

#     profiler.disable() # 停止性能分析
#     # s = StringIO() # Python 3
#     # sortby = pstats.SortKey.CUMULATIVE # 按累积时间排序
#     # ps = pstats.Stats(profiler, stream=s).sort_stats(sortby) # 创建pstats实例并排序
#     # ps.print_stats(20) # 打印前20个耗时最多的函数
#     # print(s.getvalue()) # 打印性能分析结果

#     # 或者直接保存到文件
#     profiler.dump_stats("lupa_profile_results.prof") # 保存性能分析结果到文件
#     # 之后可以用 snakeviz lupa_profile_results.prof 查看可视化结果

(注:上面的cProfile示例代码是结构性的,需要替换function_using_lupa为实际测试的代码。)

Lua Profilers: 如果怀疑是Lua脚本本身执行缓慢(尤其是未使用LuaJIT或JIT效果不佳时),可以使用Lua的profiler。

标准Lua: debug.sethook可以用来做简单的指令计数或基于时间的采样。有一些第三方Lua profiler库。
LuaJIT: 提供了强大的内置profiler (jit.profile模块)。

-- 在Lua脚本中 (如果lupa链接的是LuaJIT)
-- local profiler = require("jit.profile")
-- profiler.start("html", "profile_output.html") -- 开始性能分析,输出为HTML格式

-- -- 你的Lua代码...
-- function my_lua_heavy_function()
--     for i = 1, 1000000 do local x = math.sin(i) end
-- end
-- my_lua_heavy_function()

-- profiler.stop() -- 停止性能分析

你可以通过lupa执行启动和停止profiler的Lua代码,然后分析生成的报告。

自定义基准测试 (Micro-benchmarks): 针对特定的lupa交互模式(如不同大小数据的转换、不同类型函数的调用)编写小型的、重复执行的基准测试,使用Python的timeit模块来精确测量执行时间。

import timeit
import lupa

# setup_code = """
# import lupa
# lua = lupa.LuaRuntime()
# py_list_small = list(range(10))
# py_list_large = list(range(10000))
# lua_func_simple = lua.eval("function(a,b) return a+b end")
# lua_table_large_proxy = lua.table_from(list(range(10000)))
# def py_callback_simple(a,b): return a+b
# lua.globals().py_cb = py_callback_simple
# lua_func_calls_py_cb = lua.eval("function(a,b) return py_cb(a,b) end")
# """ # timeit的setup代码

# stmt_py_to_lua_list_small = "lua.table_from(py_list_small)" # Python列表转Lua table (小)
# stmt_py_to_lua_list_large = "lua.table_from(py_list_large)" # Python列表转Lua table (大)
# stmt_lua_call_simple = "lua_func_simple(1,2)" # 调用简单Lua函数
# stmt_lua_iterate_large_proxy = "for _ in lua_table_large_proxy.values(): pass" # 迭代大型Lua table代理
# stmt_py_call_from_lua = "lua_func_calls_py_cb(10, 20)" # Lua回调Python函数

# iterations = 1000 # 执行次数

# print(f"Timing '{stmt_py_to_lua_list_small}' for {iterations} iterations:") # 打印计时信息
# time_taken = timeit.timeit(stmt_py_to_lua_list_small, setup=setup_code, number=iterations) # 执行计时
# print(f"  Total time: {time_taken:.4f}s, Per call: {time_taken/iterations:.7f}s
") # 打印结果

# print(f"Timing '{stmt_py_to_lua_list_large}' for {iterations // 100} iterations:") # 打印计时信息 (减少大列表的迭代次数)
# time_taken_large_list = timeit.timeit(stmt_py_to_lua_list_large, setup=setup_code, number=iterations // 100) # 执行计时
# print(f"  Total time: {time_taken_large_list:.4f}s, Per call: {time_taken_large_list/(iterations//100):.7f}s
") # 打印结果

# print(f"Timing '{stmt_lua_call_simple}' for {iterations} iterations:") # 打印计时信息
# time_taken_lua_call = timeit.timeit(stmt_lua_call_simple, setup=setup_code, number=iterations) # 执行计时
# print(f"  Total time: {time_taken_lua_call:.4f}s, Per call: {time_taken_lua_call/iterations:.7f}s
") # 打印结果

# print(f"Timing '{stmt_lua_iterate_large_proxy}' for {iterations // 100} iterations:") # 打印计时信息
# time_taken_iterate_proxy = timeit.timeit(stmt_lua_iterate_large_proxy, setup=setup_code, number=iterations // 100) # 执行计时
# print(f"  Total time: {time_taken_iterate_proxy:.4f}s, Per call: {time_taken_iterate_proxy/(iterations//100):.7f}s
") # 打印结果

# print(f"Timing '{stmt_py_call_from_lua}' for {iterations} iterations:") # 打印计时信息
# time_taken_py_cb = timeit.timeit(stmt_py_call_from_lua, setup=setup_code, number=iterations) # 执行计时
# print(f"  Total time: {time_taken_py_cb:.4f}s, Per call: {time_taken_py_cb/iterations:.7f}s
") # 打印结果

(注:timeit示例同样是结构性的,需要确保setup_code和语句能正确运行。)

10.3 优化策略

减少跨语言调用的频率 (Minimize Chattiness):

批处理数据: 如果需要对集合中的许多元素执行相同的Lua操作,不要在Python循环中为每个元素调用Lua函数。而是将整个集合(或一大块)传递给Lua,让Lua在内部循环处理。

# 不推荐 (高开销)
# lua = lupa.LuaRuntime()
# lua_process_item_func = lua.eval("function(item) return item * 2 end") # Lua处理单个项目的函数
# py_list = list(range(10000)) # Python列表
# processed_list_py = [] # 处理后的列表
# for item in py_list: # 遍历Python列表
#     processed_list_py.append(lua_process_item_func(item)) # 为每个项目调用Lua函数

# 推荐 (低开销)
lua = lupa.LuaRuntime() # 创建Lua运行时实例
lua_process_list_func_str = """
function(input_table)
    local result_table = {}
    for i, item in ipairs(input_table) do
        result_table[i] = item * 2 -- Lua内部处理
    end
    return result_table
end
""" # Lua处理整个列表的函数字符串
lua_process_list_func = lua.eval(lua_process_list_func_str) # 获取Lua函数代理

py_list = list(range(10000)) # Python列表
lua_input_table = lua.table_from(py_list) # 将整个Python列表转换为Lua table

lua_result_table_proxy = lua_process_list_func(lua_input_table) # 一次调用处理整个列表

# 如果需要转回Python list (会有额外转换开销,但调用次数减少了)
# processed_list_py = [v for v in lua_result_table_proxy.values()] 
# print(f"Processed list (first 10): {processed_list_py[:10]}")

让Lua做更多工作: 设计Lua函数使其能够完成一个更完整的子任务,而不是多个细小的步骤都需要Python协调。

优化数据转换:

避免不必要的完整转换: 当从Lua获取table时,LuaTableProxy通常足够高效,因为它只在访问时转换元素。只有当你确实需要一个独立的Python dictlist副本时,才进行完整转换。
传递基本类型或简单结构: 如果可能,优先传递数字、字符串、布尔值,或者只包含这些基本类型的浅层table/dict。
序列化: 对于非常大的或复杂的数据结构,如果跨语言调用的次数不多,但数据本身很大,可以考虑使用高效的序列化格式(如MessagePack, Protocol Buffers, FlatBuffers)在Python和Lua之间传递字节串,然后在各自语言中反序列化。Lua有一些这类库(如 lua-cjson, luajit-msgpack-pure)。这避免了lupa逐元素转换的开销,但增加了序列化/反序列化的开销。需要权衡。

# Python 侧 (概念)
# import msgpack # 需要安装 msgpack-python
# lua = lupa.LuaRuntime()
# lua.execute("MsgPack = require('jit.msgpack')") # 假设使用LuaJIT的msgpack (或其他lua-msgpack库)

# data_to_send_py = {"name": "Test", "values": list(range(1000)), "active": True} # 要发送的Python数据
# packed_data_bytes = msgpack.packb(data_to_send_py) # 使用MessagePack序列化

# lua_process_packed_func = lua.eval("""
# function(packed_str)
#     local data_lua = MsgPack.unpack(packed_str)
#     -- ... 在Lua中处理 data_lua ...
#     data_lua.processed = true
#     data_lua.values_sum = 0
#     for _,v in ipairs(data_lua.values) do data_lua.values_sum = data_lua.values_sum + v end
#     return MsgPack.pack(data_lua) -- 将处理后的数据打包返回
# end
# """) # Lua处理打包数据的函数

# packed_result_bytes_lua_proxy = lua_process_packed_func(packed_data_bytes) # 调用Lua函数,传递字节串
# # packed_result_bytes_lua_proxy 返回的也是Lua string (bytes),lupa转为Python bytes
# if isinstance(packed_result_bytes_lua_proxy, bytes): # 如果返回的是字节串
#      unpacked_result_py = msgpack.unpackb(packed_result_bytes_lua_proxy) # 使用MessagePack反序列化
#      print(f"Result from Lua via MsgPack: {unpacked_result_py}") # 打印结果
# else:
#      print(f"Lua did not return packed bytes as expected: {type(packed_result_bytes_lua_proxy)}") # 打印未按预期返回信息

使用bytes: 如果要在Python和Lua之间传递原始二进制数据(如图像数据、文件内容),直接使用Python bytes类型,它会高效地映射到Lua字符串。

缓存Lua对象和函数代理:

如果一个Lua函数或table会从Python被频繁调用/访问,获取其代理一次并将其存储在Python变量中,而不是每次都通过lua.globals().func_namelua.eval("func_name")重新获取。

# lua = lupa.LuaRuntime()
# lua.execute("function my_utility(x) return x * 10 end") # 定义Lua工具函数

# 不推荐: 每次都重新获取代理
# for i in range(1000):
#     utility_func_proxy = lua.globals().my_utility # 每次循环都重新获取
#     utility_func_proxy(i) # 调用

# 推荐: 缓存代理
# cached_utility_func_proxy = lua.globals().my_utility # 获取一次并缓存
# for i in range(1000):
#     cached_utility_func_proxy(i) # 使用缓存的代理

利用LuaJIT:

确保lupa链接到LuaJIT: 这是获得显著性能提升的首要条件。
编写JIT友好的Lua代码:

避免频繁改变table的结构(例如,在循环中不断添加新字段)。
使用局部变量。
循环应该是“热”的(执行次数足够多)以便JIT编译。
避免在JIT编译的关键路径上使用过多pcallxpcall(它们会中断trace)。
查阅LuaJIT的性能指南和wiki,了解哪些模式对JIT友好,哪些是“JIT毒药”。

使用FFI: 对于能用C实现的性能关键部分,通过FFI从LuaJIT调用,通常比任何纯Lua或Python实现都快。

Lua代码优化:

即使有JIT,Lua代码本身的效率也很重要。遵循通用的Lua性能优化技巧(如局部变量缓存全局函数、table重用、选择合适的算法和数据结构等)。

Python回调优化 (Lua调用Python):

如果Lua需要频繁回调Python,确保被回调的Python函数尽可能高效。
如果回调非常简单(例如只是返回一个常量或执行简单计算),考虑是否可以在Lua中直接实现,或者Python在初始化时将结果计算好并传递给Lua,而不是让Lua每次都回调。

LuaRuntime实例管理:

创建LuaRuntime (luaL_newstate)是有开销的。对于需要长期运行或频繁与Lua交互的应用,应复用LuaRuntime实例,而不是为每个小任务都创建一个新的。
如果需要隔离,可以在同一个LuaRuntime中使用不同的Lua全局环境表(沙箱),而不是创建多个LuaRuntime实例(除非需要彻底的VM级隔离或并行执行)。

选择合适的unpack_returned_tuples:

unpack_returned_tuples=True (默认): 如果Lua函数返回多个值,lupa会将它们解包成Python元组。如果只返回一个值,则直接返回该值。这在某些情况下方便,但如果Lua函数可能返回单个table而你期望它保持为单个代理(而不是被视为多返回值的唯一元素),可能会产生意外。
unpack_returned_tuples=False: Lua函数的所有返回值总是作为一个Python元组返回(即使只有一个返回值,也是单元素元组;如果Lua无返回值,则是空元组)。这提供了更一致和可预测的行为,尤其是在处理pcall的结果或协程resume时。对于性能,这个设置本身影响不大,但选择错误可能导致额外的Python代码来处理不期望的返回结构。

直接在Lua中处理迭代:
如果你有一个Lua table,并且想在Python中对其进行迭代处理,通常比在Python中通过LuaTableProxy.items()values()迭代更高效的做法是:编写一个Lua函数,该函数接收这个table和一个Lua回调函数作为参数,然后在Lua内部迭代table并对每个元素调用Lua回调。Python侧只需要调用这个主迭代Lua函数一次。

# lua = lupa.LuaRuntime()
# lua_script = """
# function map_table_in_lua(input_table, transform_func_lua)
#     local result_table = {}
#     for k, v in pairs(input_table) do
#         result_table[k] = transform_func_lua(v, k) -- Lua回调接收值和键
#     end
#     return result_table
# end

# function double_value(val, key)
#     -- print("Lua doubling value for key:", key)
#     return val * 2
# end
# """ # Lua脚本,包含map_table_in_lua和double_value函数
# lua.execute(lua_script) # 执行Lua脚本

# lua_map_func = lua.globals().map_table_in_lua # 获取map_table_in_lua函数代理
# lua_transform_func = lua.globals().double_value # 获取double_value函数代理

# py_data = {"a": 1, "b": 2, "c": 30} # Python数据
# lua_input_table = lua.table_from(py_data) # 将Python数据转换为Lua table

# -- 调用Lua函数,让它在Lua中完成迭代和转换
# lua_result_proxy = lua_map_func(lua_input_table, lua_transform_func) # 调用map_table_in_lua函数

# result_py = {k:v for k,v in lua_result_proxy.items()} # 将结果代理转换为Python字典
# print(f"Result after Lua in-loop processing: {result_py}") # 打印结果
# # 预期: {'a': 2, 'b': 4, 'c': 60}

这种方式将迭代逻辑完全放在Lua中,只进行一次主函数调用和一次结果转换,避免了Python循环中对代理的多次访问。

考虑Cython与lupa结合:
如果Python侧的逻辑是性能瓶颈,并且涉及到与lupa代理对象的复杂交互,可以考虑使用Cython来编写这部分Python代码。Cython代码可以更接近C级别操作Python对象和lupa的C API(如果lupa暴露了稳定的C API供扩展使用,但这不常见),可能会减少一些Python解释器的开销。但这增加了复杂性。

10.4 何时进行优化?

不要过早优化: 首先确保代码正确、可读、可维护。
基于性能分析: 只有当性能分析(profiling)明确指出lupa交互是瓶颈时,才投入精力进行针对性的优化。
权衡: 某些优化(如使用序列化传递数据)可能会增加代码复杂性,需要权衡性能提升与开发/维护成本。

第十一章:跨越边界的挑战 —— lupa 中的错误处理与调试

在Python和Lua的混合编程环境中,错误可能发生在Python端,也可能发生在Lua端,或者在两者交互的过程中。理解错误如何传播、如何捕获以及如何调试这些跨语言的调用,对于构建稳定和可靠的应用程序至关重要。

11.1 错误处理的复杂性

不同的错误模型: Python使用异常(exceptions)作为其主要的错误处理机制,而Lua通常使用返回nil和错误消息(或错误码)的约定,并通过error()函数抛出更严重的错误,pcallxpcall用于安全调用。
堆栈跟踪的割裂: 当错误从一种语言传播到另一种语言时,原始语言的堆栈跟踪信息可能不完整或难以直接在目标语言中解读。
数据转换的潜在问题: 在数据传递过程中,如果发生类型不匹配或转换失败,也可能引发错误,这些错误的定位可能比较棘手。
资源管理: 如果在Lua中分配的资源(例如,通过FFI打开的文件句柄)由于Python端的错误而未能正确释放,可能导致资源泄漏。

lupa 作为一个桥接库,在这些方面做了大量工作,试图以一种合理的方式将两种语言的错误模型统一起来。

11.2 Lua 错误在 Python 中的捕获与解析

当在Python中通过lupa执行的Lua代码发生错误时,lupa 通常会将Lua错误包装成一个Python异常并抛出。最常见的异常类型是 lupa.LuaError

11.2.1 lupa.LuaError 异常

lupa.LuaErrorRuntimeError 的一个子类。当Lua代码中发生错误(例如语法错误、运行时错误、通过error()函数显式抛出的错误),并且这个错误没有在Lua层面被pcallxpcall捕获时,lupa会捕获这个Lua错误,并将其转换为lupa.LuaError在Python中抛出。

这个异常对象通常会包含来自Lua的原始错误信息。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime(unpack_returned_tuples=True) # 创建Lua运行时实例

print("--- 演示Lua语法错误 ---")
try:
    lua.eval("print('Hello' -- 这是一个未闭合的字符串") # 尝试执行有语法错误的Lua代码
except lupa.LuaError as e: # 捕获lupa抛出的LuaError
    print(f"捕获到Lua语法错误 (Python端): {
              e}") # 打印错误信息
    # e 对象通常直接包含了Lua返回的错误字符串

print("
--- 演示Lua运行时错误 ---")
try:
    lua.execute("""
    function cause_error()
        local a = nil
        return a.field -- 尝试对nil值进行字段访问,这将导致运行时错误
    end
    cause_error()
    """) # 定义并执行一个会导致运行时错误的Lua函数
except lupa.LuaError as e: # 捕获lupa抛出的LuaError
    print(f"捕获到Lua运行时错误 (Python端): {
              e}") # 打印错误信息
    # 错误信息通常是 "attempt to index a nil value (local 'a')" 之类的

print("
--- 演示Lua中显式error()调用 ---")
try:
    lua.eval("error('这是一个由Lua error()函数抛出的自定义错误')") # 执行Lua的error()函数
except lupa.LuaError as e: # 捕获lupa抛出的LuaError
    print(f"捕获到Lua error()调用 (Python端): {
              e}") # 打印错误信息

print("
--- 访问Lua的nil值引发的属性错误 ---")
lua_func_returns_nil = lua.eval("function() return nil end") # 定义一个返回nil的Lua函数
val = lua_func_returns_nil() # 调用该函数
try:
    print(val.non_existent_attribute) # 尝试访问nil对应的Python代理对象的属性
except AttributeError as e: # 在Python中,对Lua nil的无效操作通常是AttributeError
    print(f"在Python中访问Lua nil对象的属性时捕获到AttributeError: {
              e}") # 打印错误信息

print("
--- 演示从Python调用不存在的Lua函数 ---")
try:
    non_existent_func = lua.globals().non_existent_function # 尝试获取一个不存在的Lua全局函数
    if non_existent_func is None: # lupa 对于不存在的全局变量/函数,获取时会返回None
        print("Lua全局函数 'non_existent_function' 不存在 (返回None)")
    # 如果直接调用 lua.eval("non_existent_function()") 则会是 LuaError
    lua.eval("non_existent_function()") # 直接尝试执行不存在的函数
except lupa.LuaError as e:
    print(f"尝试执行不存在的Lua函数时捕获到LuaError: {
              e}") # 打印错误信息

代码解释:

import lupafrom lupa import LuaRuntime: 导入必要的lupa库。
lua = LuaRuntime(unpack_returned_tuples=True): 初始化Lua运行时。
try...except lupa.LuaError as e:: 这是捕获从Lua传递过来的错误的标准Python模式。
lua.eval("print('Hello' -- 这是一个未闭合的字符串"): 这行Lua代码因为字符串没有正确闭合,会导致Lua端的解析错误(语法错误)。lupa会将其转换为LuaError
lua.execute(...): 执行一段多行Lua代码,其中a.field会因为anil而导致Lua运行时错误。
lua.eval("error('这是一个由Lua error()函数抛出的自定义错误')"): Lua的error()函数会立即终止脚本并将参数作为错误信息。lupa同样会捕获它。
lua_func_returns_nil = lua.eval("function() return nil end"): 定义一个返回nil的Lua函数。
val = lua_func_returns_nil(): 调用后,val在Python中是None
val.non_existent_attribute: 对None(代表Lua的nil)尝试进行属性访问,这在Python层面通常会引发AttributeError,而不是lupa.LuaError,因为此时操作的是Python的None对象。
non_existent_func = lua.globals().non_existent_function: 当试图通过lua.globals()访问一个不存在的Lua全局变量或函数时,lupa通常会返回Python的None,而不是立即抛出错误。
lua.eval("non_existent_function()"): 如果直接在Lua代码中尝试调用一个不存在的函数,Lua会报错,lupa会将其转换为LuaError

11.2.2 从 LuaError 中提取信息

LuaError 对象本身通常就包含了Lua解释器提供的错误消息字符串。在很多情况下,这个字符串已经足够信息丰富。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime() # 创建Lua运行时实例

lua_script_with_traceback = """
function level_three()
    error("错误发生在第三层嵌套调用") -- 在第三层函数中抛出错误
end

function level_two()
    level_three() -- 调用第三层函数
end

function level_one()
    level_two() -- 调用第二层函数
end
"""
lua.execute(lua_script_with_traceback) # 执行定义了多层函数的Lua脚本

try:
    lua.globals().level_one() # 从Python调用顶层Lua函数
except lupa.LuaError as e: # 捕获LuaError
    print(f"捕获到Lua错误 (Python端):")
    print(f"  错误消息: {
              str(e)}") # 直接将异常对象转为字符串通常能得到Lua的错误信息
    # lupa.LuaError 的字符串表示形式通常包含了Lua的错误消息和部分堆栈信息
    # 例如: "[string "..."]:2: 错误发生在第三层嵌套调用
stack traceback:
	[string "..."]:2: in function 'level_three'
	[string "..."]:6: in function 'level_two'
	[string "..."]:10: in function 'level_one'
	[C]: in ?
    
    # 虽然 lupa.LuaError 本身不直接暴露一个结构化的堆栈对象,
    # 但其 __str__ 方法通常包含了Lua格式的堆栈回溯。
    # 如果需要更细致地解析,可能需要字符串处理。
    print("
  原始错误信息 (通常包含Lua堆栈回溯):")
    for line in str(e).splitlines(): # 逐行打印错误信息
        print(f"    {
              line}")

代码解释:

lua_script_with_traceback: 定义了一个包含多层函数调用的Lua脚本,最终在level_three函数中通过error()抛出错误。
lua.globals().level_one(): Python调用Lua中的level_one函数,这将触发错误。
str(e): 将捕获到的lupa.LuaError对象e转换为字符串。lupa在实现LuaError__str__方法时,通常会包含Lua解释器提供的原始错误消息和堆栈回溯信息。
str(e).splitlines(): 将包含堆栈信息的多行错误字符串分割成单独的行进行打印,便于阅读。

Lua的堆栈回溯(stack traceback)对于定位错误在Lua代码中的位置至关重要。lupa尽力将这些信息传递给Python。

11.2.3 Lua 编译/加载错误

如果提供的Lua代码本身有语法错误,或者require一个不存在的模块或有错误的模块时,也会产生LuaError

import lupa
from lupa import LuaRuntime

lua = LuaRuntime() # 创建Lua运行时实例

print("--- 演示加载包含语法错误的Lua文件 ---")
# 假设我们有一个名为 'buggy_module.lua' 的文件,内容如下:
# -- buggy_module.lua
# local M = {}
# function M.greet(name)
#  return "Hello, " .. name -- 缺少一个 'end'
# return M

# 为了演示,我们动态创建这个文件内容
buggy_lua_content = """
local M = {}
function M.greet(name)
 return "Hello, " .. name -- 语法错误:函数定义缺少 'end'
-- 故意的语法错误,这里本应是 'end'
return M
"""
try:
    # lua.require('buggy_module') # 如果 buggy_module.lua 存在于 LUA_PATH
    # 为了自包含示例,我们直接执行内容
    lua.execute(buggy_lua_content) # 执行包含语法错误的Lua代码
except lupa.LuaError as e: # 捕获LuaError
    print(f"加载/执行Lua代码时捕获到编译错误 (Python端): {
              e}")
    # 错误信息可能像:"[string "..."]:3: 'end' expected (to close 'function' at line 2) near <eof>"

print("
--- 演示 require 不存在的模块 ---")
try:
    # 注意:要使require失败且能被捕获,通常需要确保LUA_PATH配置正确
    # 且目标模块确实不存在或无法被Lua的搜索机制找到。
    # 简单地 require 一个随机名可能不会报错,而是返回nil,
    # 真正的错误可能在尝试使用返回的nil时发生。
    # 为了更明确地触发 loadlib 错误 (如果适用),或者文件找不到的错误
    lua.eval("require('a_module_that_absolutely_does_not_exist_anywhere')") # 尝试加载一个不存在的Lua模块
except lupa.LuaError as e: # 捕获LuaError
    print(f"require一个不存在的模块时捕获到错误 (Python端): {
              e}")
    # 错误信息可能像: "module 'a_module_that_absolutely_does_not_exist_anywhere' not found:..."

代码解释:

buggy_lua_content: 包含一个明显语法错误的Lua代码字符串(函数定义缺少end)。
lua.execute(buggy_lua_content): 当lupa尝试编译并执行这段有语法错误的Lua代码时,Lua解释器会报告一个编译错误,lupa将其转换为LuaError
lua.eval("require('a_module_that_absolutely_does_not_exist_anywhere')"): Lua的require函数在找不到模块时会抛出错误。lupa会捕获这个错误。错误消息会指明模块未找到以及搜索过的路径。

11.3 Python 错误在 Lua 中的体现

当Lua代码调用一个通过lupa传递过去的Python函数,而这个Python函数内部抛出了一个异常时,lupa需要决定如何将这个Python异常传递回Lua执行环境。

通常,lupa会尝试将Python异常转换为一个Lua错误。这意味着在Lua端,这个调用看起来就像是执行了一个会调用error()的Lua函数。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime(unpack_returned_tuples=True) # 创建Lua运行时实例

# 定义一个会抛出Python异常的Python函数
def python_function_that_raises():
    print("Python: python_function_that_raises 被调用") # 打印被调用的信息
    raise ValueError("一个来自Python的ValueError") # 抛出一个ValueError异常

# 将Python函数注册到Lua全局环境中
lua.globals().py_raiser = python_function_that_raises # 将Python函数赋给Lua全局变量

print("--- 尝试从Lua调用会抛出Python异常的函数 (不使用pcall) ---")
# 注意:直接这样调用,如果Python函数抛异常,会导致lupa.LuaError在Python端被再次抛出
# 因为Lua错误(由Python异常转换而来)未在Lua内部处理
try:
    lua.execute("""
    print("Lua: 准备调用 py_raiser()")
    py_raiser() -- 调用Python函数,该函数会抛出异常
    print("Lua: py_raiser() 调用完毕 (如果未发生错误)") -- 如果py_raiser()抛异常,这行不会执行
    """)
except lupa.LuaError as e: # 捕获lupa.LuaError
    print(f"Python端捕获到LuaError (源自Python异常): {
              e}")
    # 错误信息通常会包含Python异常的类型和消息,例如:
    # "ValueError: 一个来自Python的ValueError"
    # 或者可能被lupa包装得更具体,指明是Python回调中发生的错误。

print("
--- 使用Lua的pcall处理Python函数抛出的异常 ---")
# 这次,我们在Lua中使用pcall来安全地调用可能出错的Python函数
# pcall (protected call) 会捕获其调用的函数内部发生的错误
lua_script = """
print("Lua: 准备使用pcall调用 py_raiser()")
local success, result_or_error = pcall(py_raiser) -- 使用pcall安全调用Python函数
if success then
    print("Lua: pcall成功,结果:", result_or_error) -- 如果成功,打印结果
else
    print("Lua: pcall失败,错误信息:", result_or_error) -- 如果失败,打印错误信息
    -- result_or_error 将会是类似 "ValueError: 一个来自Python的ValueError" 的字符串
end
return success, result_or_error -- 将pcall的结果返回给Python
"""

success, result_or_error = lua.execute(lua_script) # 执行Lua脚本并获取返回值

print(f"Python端接收到pcall的结果: success={
              success}, result_or_error='{
              result_or_error}'") # 打印结果

if not success:
    # result_or_error 是Lua错误信息字符串,源自Python的异常
    print(f"  具体的错误信息是: {
              result_or_error}")
    assert "ValueError: 一个来自Python的ValueError" in str(result_or_error) # 断言错误信息符合预期

# 另一个例子:Python函数返回多个值,其中一个是错误指示
def python_func_returns_error_tuple(should_fail):
    print(f"Python: python_func_returns_error_tuple(should_fail={
              should_fail}) 被调用")
    if should_fail:
        return None, "Python模拟的错误信息" # 返回None和错误信息
    return "成功的数据", None # 返回成功数据和None

lua.globals().py_error_sim = python_func_returns_error_tuple # 注册到Lua

lua_script_check_py_error = """
print("Lua: 调用 py_error_sim(true)")
local data, err = py_error_sim(true) -- 调用模拟失败的Python函数
if err then
    print("Lua: py_error_sim(true) 返回错误:", err) -- 打印错误信息
else
    print("Lua: py_error_sim(true) 返回数据:", data) -- 打印数据
end

print("Lua: 调用 py_error_sim(false)")
data, err = py_error_sim(false) -- 调用模拟成功的Python函数
if err then
    print("Lua: py_error_sim(false) 返回错误:", err) -- 打印错误信息
else
    print("Lua: py_error_sim(false) 返回数据:", data) -- 打印数据
end
"""
lua.execute(lua_script_check_py_error) # 执行Lua脚本

代码解释:

python_function_that_raises(): 一个简单的Python函数,它会明确地抛出ValueError
lua.globals().py_raiser = python_function_that_raises: 将此Python函数暴露给Lua。
第一个lua.execute块:当Lua直接调用py_raiser()时,python_function_that_raises抛出的ValueError会被lupa捕获。lupa随后会在Python端抛出一个lupa.LuaError,其消息通常会包含原始Python异常的类型和消息。这是因为Lua的执行被一个源自Python的错误打断了。
第二个lua_script块(使用pcall):

local success, result_or_error = pcall(py_raiser): Lua的pcall函数用于“保护模式调用”。如果py_raiser(即我们会抛异常的Python函数)正常执行完毕,successtrueresult_or_error是其返回值。如果py_raiser内部发生错误(Python异常被lupa转换为Lua错误),successfalseresult_or_error是错误对象(通常是错误消息字符串)。
if success then ... else ... end: Lua代码根据pcall的结果来判断Python调用是否成功,并打印相应信息。
return success, result_or_error: 将pcall的结果返回给Python调用者。

success, result_or_error = lua.execute(lua_script): Python接收到Lua脚本的返回值。
python_func_returns_error_tuple()lua_script_check_py_error: 这个例子展示了另一种常见的错误传递模式,即Python函数不抛出异常,而是返回一个指示成功/失败的值(例如元组 (data, error_message)),Lua代码可以检查这个错误信息。这是推荐的跨语言错误传递方式之一,因为它更明确。

关键点: 当Python函数被Lua调用并抛出未被Python内部捕获的异常时,lupa通常会:

捕获这个Python异常。
将其转换为一个Lua错误(本质上像是在Lua中调用了error("Python Exception: ..."))。
如果这个Lua错误没有被Lua代码中的pcallxpcall处理,那么它将中止Lua脚本的执行,并作为lupa.LuaError重新传播回最初发起调用的Python代码。

因此,在Lua中调用可能失败的Python函数时,使用pcall是一种良好的防御性编程实践。

11.4 调试技巧

调试跨语言的应用程序可能比单语言应用程序更复杂。以下是一些策略和技巧:

11.4.1 Python 端调试

使用标准Python调试器 (pdb, IDE调试器):

可以在调用lua.eval()lua.execute()或调用从Lua获取的函数代理对象之前/之后设置断点。
可以检查传递给Lua的Python对象和从Lua返回的对象。
对于lupa.LuaError,可以检查异常对象的内容。
直接单步进入lupa内部或Lua代码通常是不可能的,因为Lua在自己的运行时中执行。

import lupa
from lupa import LuaRuntime
import pdb # 导入Python调试器

lua = LuaRuntime() # 创建Lua运行时

def python_callback_for_lua(arg1, arg2):
    print(f"Python: python_callback_for_lua received: {
                arg1}, {
                arg2}") # 打印接收到的参数
    # pdb.set_trace() # 在这里设置断点,当Lua调用此回调时会暂停
    if not isinstance(arg1, str): # 检查参数类型
        raise TypeError("Argument 1 must be a string") # 如果类型不符,抛出类型错误
    return f"Processed: {
                arg1}-{
                arg2}" # 返回处理后的字符串

lua.globals().py_cb = python_callback_for_lua # 将回调注册到Lua

lua_script = """
print("Lua: Calling py_cb with correct types")
local res1 = py_cb("hello", 123) -- 使用正确类型调用Python回调
print("Lua: py_cb result 1:", res1)

print("Lua: Calling py_cb with incorrect type, expecting error")
local success, err_or_res = pcall(py_cb, 456, "world") -- 使用错误类型调用,并用pcall捕获
if not success then
    print("Lua: py_cb errored as expected:", err_or_res) -- 打印错误信息
else
    print("Lua: py_cb succeeded unexpectedly:", err_or_res) -- 打印意外成功的信息
end
"""
# pdb.set_trace() # 在执行Lua脚本前设置断点
lua.execute(lua_script) # 执行Lua脚本
print("Python: Lua script execution finished.") # 打印脚本执行完毕信息

代码解释:

import pdb: 导入Python的命令行调试器。
pdb.set_trace(): 在代码中插入此行,当执行到这里时,程序会暂停,并进入pdb的交互式调试环境。你可以检查变量,单步执行等。
python_callback_for_lua中设置断点,可以观察从Lua传递过来的参数arg1, arg2的值和类型。
lua.execute(lua_script)之前设置断点,可以检查执行前的Lua状态(间接通过lua对象的方法)或Python环境。

详尽的日志记录 (Logging): 在Python代码与Lua交互的关键点(调用Lua之前、之后,回调函数入口、出口)添加日志,记录传递的数据和执行流程。

import lupa
from lupa import LuaRuntime
import logging # 导入日志模块

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') # 配置日志基础设置
lua = LuaRuntime() # 创建Lua运行时

def complex_python_logic(data_from_lua):
    logging.debug(f"Python: complex_python_logic received: {
                data_from_lua}") # 记录接收到的数据
    # ... 复杂的处理逻辑 ...
    result = {
              "status": "ok", "processed_value": str(data_from_lua).upper()} # 模拟处理结果
    logging.debug(f"Python: complex_python_logic returning: {
                result}") # 记录返回的数据
    return result # 返回结果

lua.globals().py_complex = complex_python_logic # 注册到Lua

lua_code = """
local data = {key = "value", num = 123}
logging.debug("Lua: Calling py_complex with data: " .. serpent.block(data)) -- 假设Lua有serpent库用于序列化
local py_result = py_complex(data)
logging.debug("Lua: Received from py_complex: " .. serpent.block(py_result))
"""
# 为了简单起见,我们让Lua的logging也通过Python的print,实际中Lua会有自己的日志方案
# 或者Python提供一个日志函数给Lua调用
def lua_log(level, message):
    if level == "debug":
        logging.debug(f"LUA_LOG: {
                message}") # Lua的调试日志
    elif level == "info":
        logging.info(f"LUA_LOG: {
                message}") # Lua的信息日志
    # ... 其他级别
lua.globals().log_from_lua = lua_log # 将Python日志函数暴露给Lua

lua_code_with_py_log = """
local data = {key = "value", num = 123}
log_from_lua("debug", "Calling py_complex with data: key=" .. data.key .. ", num=" .. data.num) -- 调用Python日志函数
local py_result = py_complex(data) -- 调用Python复杂逻辑函数
log_from_lua("debug", "Received from py_complex: status=" .. py_result.status) -- 调用Python日志函数
"""
logging.info("Python: About to execute Lua code.") # 执行Lua代码前的日志
lua.execute(lua_code_with_py_log) # 执行Lua代码
logging.info("Python: Lua code execution finished.") # 执行Lua代码后的日志

代码解释:

import logginglogging.basicConfig(...): 设置Python的日志系统。
logging.debug(...), logging.info(...): 在Python代码的关键点记录日志信息。
lua_log(level, message)lua.globals().log_from_lua = lua_log: 创建一个Python函数,允许Lua代码通过它来写入Python的日志系统,这样可以将两边的日志统一管理。
在Lua代码中,通过调用log_from_lua来记录其执行阶段和数据状态。

11.4.2 Lua 端调试

print() 大法: 在Lua脚本中大量使用print()语句输出变量值、执行路径等。lupa通常会将Lua的print输出重定向到Python的标准输出(或可配置)。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime() # 创建Lua运行时实例

lua_script_with_prints = """
print("Lua: Script started") -- 打印脚本开始信息
local var1 = "Lua variable"
local var2 = { nested = true, value = 42 }
print("Lua: var1 =", var1) -- 打印变量var1的值
-- 对于表,直接print可能只显示类型和地址,需要辅助函数打印内容
-- 或者逐个字段打印
print("Lua: var2.nested =", var2.nested, ", var2.value =", var2.value) -- 打印表var2的字段值

function my_lua_func(p1, p2)
    print("Lua: my_lua_func called with:", p1, p2) -- 打印函数调用参数
    local result = p1 + p2
    print("Lua: my_lua_func result:", result) -- 打印函数结果
    return result
end

local sum_res = my_lua_func(10, 20) -- 调用自定义Lua函数
print("Lua: Sum result in main script:", sum_res) -- 打印主脚本中的结果
print("Lua: Script ended") -- 打印脚本结束信息
"""
print("Python: Executing Lua script with print statements...")
lua.execute(lua_script_with_prints) # 执行带print的Lua脚本
print("Python: Lua script execution finished.")

代码解释:

lua_script_with_prints: Lua脚本中包含多个print()语句,用于追踪执行流程和变量状态。
lua.execute(lua_script_with_prints)执行时,这些Lua的print输出通常会出现在Python的控制台。

Lua 的 debug: Lua内置了一个debug库,提供了一些底层调试功能,如获取堆栈跟踪 (debug.traceback())、设置钩子函数等。通过lupa,你可以在一定程度上从Python侧触发或在Lua侧使用这些功能。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime() # 创建Lua运行时实例

# 示例:从Python获取Lua的堆栈跟踪
lua_script_for_traceback = """
function func_c()
    -- 我们想在这里获取堆栈跟踪
    local tb = debug.traceback("Current Lua stack:", 2) -- 获取堆栈跟踪,2表示从调用traceback的上一层开始
    print(tb) -- 在Lua中打印堆栈跟踪
    return tb -- 将堆栈跟踪返回给Python
end

function func_b()
    return func_c() -- 调用func_c
end

function func_a()
    return func_b() -- 调用func_b
end
"""
lua.execute(lua_script_for_traceback) # 执行定义了函数的Lua脚本

get_lua_traceback = lua.globals().func_a # 获取Lua函数

print("Python: Calling Lua function to get traceback...")
traceback_str = get_lua_traceback() # 调用Lua函数,该函数内部会调用debug.traceback

print("
Python: Received traceback string from Lua:")
print(traceback_str) # 打印从Lua返回的堆栈跟踪字符串

# 示例:在Lua中使用xpcall获取更详细的错误和堆栈
lua_error_script_for_xpcall = """
function cause_lua_error()
    local x = nil
    return x.y -- 这会产生一个错误
end

function error_handler(err_msg)
    local tb = debug.traceback(err_msg, 2) -- 生成堆栈跟踪,将原始错误信息作为消息前缀
    print("Lua xpcall error_handler: Original error:", err_msg) -- 打印原始错误信息
    print("Lua xpcall error_handler: Traceback follows:") -- 打印跟踪信息提示
    print(tb) -- 打印堆栈跟踪
    return tb -- 返回包含堆栈的错误信息
end

function safe_call_lua_error()
    print("Lua: Attempting to call cause_lua_error() via xpcall")
    local status, result = xpcall(cause_lua_error, error_handler) -- 使用xpcall安全调用,并指定错误处理函数
    if not status then
        print("Lua: xpcall caught an error. Processed error message:") -- 打印错误捕获信息
        print(result) -- 打印处理后的错误信息(包含堆栈)
    else
        print("Lua: xpcall call was successful (unexpected). Result:", result) -- 打印意外成功信息
    end
    return status, result -- 返回状态和结果/错误
end
"""
lua.execute(lua_error_script_for_xpcall) # 执行定义了xpcall逻辑的Lua脚本
safe_caller = lua.globals().safe_call_lua_error # 获取Lua函数

print("
Python: Calling Lua function that uses xpcall...")
status, res_or_err = safe_caller() # 调用Lua函数
print(f"Python: xpcall from Lua returned: status={
                status}") # 打印状态
print("Python: xpcall from Lua returned result/error:") # 打印结果/错误
print(res_or_err) # 打印具体内容
assert not status #断言状态为失败
assert "attempt to index a nil value" in str(res_or_err) # 断言错误信息包含预期内容

代码解释:

debug.traceback("Current Lua stack:", 2): Lua的debug.traceback函数用于生成当前的调用堆栈。第一个参数是可选的消息前缀,第二个参数level指示从调用栈的哪一层开始回溯。
xpcall(function_to_call, error_handler_function): Lua的xpcall类似于pcall,但它允许你提供一个自定义的错误处理函数。当function_to_call发生错误时,error_handler_function会被调用,接收原始错误对象(通常是错误消息)作为参数。错误处理函数可以做更复杂的事情,比如记录日志、生成更详细的堆栈跟踪等。
error_handler中,我们再次使用debug.traceback来获取包含原始错误位置的完整堆栈,并将其返回。这比pcall默认返回的错误信息更丰富。

外部 Lua 调试器: 对于非常复杂的Lua逻辑,可以考虑使用专门的Lua调试器(如ZeroBrane Studio的远程调试功能,或基于MobDebug的调试器)。将lupa与外部Lua调试器集成可能需要一些高级设置,例如确保Lua调试器可以附加到lupa创建的Lua状态,或者lupa在启动Lua时加载了调试服务器。这通常比较高级,具体取决于lupa的内部实现和调试器的能力。

11.4.3 跨语言调试策略

隔离问题: 首先确定问题是在Python端、Lua端,还是在两者交互的接口处。

单元测试: 分别为Python模块和Lua模块编写单元测试。
简化场景: 如果在复杂交互中出错,尝试创建一个最小的可复现示例,只包含出错的部分。

接口清晰: 设计Python和Lua之间的清晰、简单的接口。避免过于复杂的数据结构或回调链,这会增加调试难度。
一致的错误报告: 约定一种错误报告机制。例如,Lua函数总是返回 (success_flag, result_or_error_message),或者Python回调在出错时总是抛出特定类型的异常。
日志对齐: 如果同时在Python和Lua中记录日志,确保日志的时间戳或请求ID能够关联起来,以便追踪一个操作在两种语言中的完整流程。

11.5 高级错误处理模式

11.5.1 在 Python 中处理 Lua pcall/xpcall 的结果

当Lua代码使用pcallxpcall来包裹对可能失败的操作(包括对Python回调的调用)时,Python端会收到这些安全调用的结果。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime(unpack_returned_tuples=True) # 创建Lua运行时实例

def py_may_fail(succeed):
    print(f"Python: py_may_fail(succeed={
              succeed}) called.") # 打印调用信息
    if succeed:
        return "Python function succeeded", 123 # 成功时返回多个值
    else:
        raise RuntimeError("Python function deliberately failed!") # 失败时抛出异常

lua.globals().py_may_fail = py_may_fail # 注册到Lua

lua_script_using_pcall = """
function call_python_safely(do_succeed)
    print("Lua: Calling py_may_fail via pcall, do_succeed =", do_succeed)
    -- 调用Python函数py_may_fail,参数是do_succeed
    local success, val1, val2_or_err_msg = pcall(py_may_fail, do_succeed)
    if success then
        print("Lua: pcall successful. val1:", val1, "val2:", val2_or_err_msg) -- 打印成功信息和值
        return true, val1, val2_or_err_msg -- 返回成功状态和值
    else
        print("Lua: pcall failed. Error message:", val1) -- 注意:pcall失败时,第一个返回值是false,第二个是错误消息
        return false, val1 -- 返回失败状态和错误信息
    end
end
"""
lua.execute(lua_script_using_pcall) # 执行定义了安全调用函数的Lua脚本
lua_safe_caller = lua.globals().call_python_safely # 获取Lua函数

print("
Python: Testing successful pcall from Lua...")
s, v1, v2 = lua_safe_caller(True) # 调用Lua函数,使其成功调用Python函数
print(f"Python: Received from Lua: success={
              s}, val1='{
              v1}', val2={
              v2}") # 打印结果
assert s is True # 断言成功
assert v1 == "Python function succeeded" # 断言值正确
assert v2 == 123 # 断言值正确

print("
Python: Testing failed pcall from Lua...")
s, err_msg = lua_safe_caller(False) # 调用Lua函数,使其调用Python函数失败
print(f"Python: Received from Lua: success={
              s}, error_message='{
              err_msg}'") # 打印结果
assert s is False # 断言失败
assert "Python function deliberately failed!" in str(err_msg) # 断言错误信息正确

代码解释:

py_may_fail(succeed): 一个Python函数,根据参数决定是正常返回还是抛出异常。
lua_script_using_pcall: Lua脚本中的call_python_safely函数使用pcall来调用py_may_fail

local success, val1, val2_or_err_msg = pcall(py_may_fail, do_succeed):

如果py_may_fail成功(即do_succeedtrue),successtrueval1val2_or_err_msgpy_may_fail返回的两个值。
如果py_may_fail失败(抛异常),successfalseval1是错误消息字符串(由lupa从Python异常转换而来),val2_or_err_msg在这种情况下通常是nilpcall失败时只返回false和错误对象)。

Python代码随后调用lua_safe_caller并检查返回的success标志和相应的值或错误信息。

11.5.2 自定义错误对象或代码

为了更结构化地处理错误,可以定义自己的错误代码或错误对象,并在Python和Lua之间传递。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime(unpack_returned_tuples=True) # 创建Lua运行时实例

# Python端的错误代码定义
class PythonErrorCodes:
    INVALID_INPUT = 1001
    RESOURCE_UNAVAILABLE = 1002
    INTERNAL_ERROR = 1003

def python_service_with_error_codes(input_val):
    print(f"Python: python_service_with_error_codes received: {
              input_val}")
    if not isinstance(input_val, int):
        # 返回错误码和错误信息表
        return False, {
            "code": PythonErrorCodes.INVALID_INPUT, "message": "Input must be an integer"}
    if input_val < 0:
        # 返回错误码和错误信息表
        return False, {
            "code": PythonErrorCodes.RESOURCE_UNAVAILABLE, "message": "Cannot process negative numbers (simulating resource issue)"}
    
    # 成功情况
    return True, {
            "data": f"Processed value: {
              input_val * 2}"} # 返回成功和数据表

lua.globals().py_service = python_service_with_error_codes # 注册到Lua

lua_script_handling_custom_errors = """
-- Lua端也可以定义对应的错误代码或直接使用Python传过来的
local PythonErrorCodes_from_lua = {
    INVALID_INPUT = 1001,
    RESOURCE_UNAVAILABLE = 1002
}

function call_py_service_and_check(val)
    print("Lua: Calling py_service with value:", val)
    local success, result_or_error_info = py_service(val) -- 调用Python服务函数

    if success then
        print("Lua: py_service call successful. Data:", result_or_error_info.data) -- 打印成功数据
        return true, result_or_error_info.data -- 返回成功状态和数据
    else
        print("Lua: py_service call failed.") -- 打印失败信息
        print("  Error Code:", result_or_error_info.code) -- 打印错误码
        print("  Error Message:", result_or_error_info.message) -- 打印错误消息

        if result_or_error_info.code == PythonErrorCodes_from_lua.INVALID_INPUT then
            print("  Lua: Detected INVALID_INPUT error type.") -- 检测到特定错误类型
        elseif result_or_error_info.code == PythonErrorCodes_from_lua.RESOURCE_UNAVAILABLE then
            print("  Lua: Detected RESOURCE_UNAVAILABLE error type.") -- 检测到特定错误类型
        end
        return false, result_or_error_info -- 返回失败状态和错误信息表
    end
end
"""
lua.execute(lua_script_handling_custom_errors) # 执行定义了错误处理逻辑的Lua脚本
lua_service_caller = lua.globals().call_py_service_and_check # 获取Lua函数

print("
Python: Testing service call with valid input...")
s, data = lua_service_caller(10) # 使用有效输入调用
assert s is True and data == "Processed value: 20" # 断言结果正确

print("
Python: Testing service call with invalid input type...")
s, err_info_py = lua_service_caller("not_an_int") # 使用无效输入类型调用
err_info = lua.table_to_list(err_info_py) if lupa.lua_type(err_info_py) == 'table' else err_info_py # lupa有时返回代理,转一下
# 或者直接用 err_info_py.code / err_info_py.message 如果它是代理对象
# 假设err_info_py已经是一个可以按键访问的Python字典或类似的lupa代理
assert s is False # 断言失败
# lupa会将Lua表作为lupa.LuaTableProxy返回,可以直接用键访问
assert err_info_py['code'] == PythonErrorCodes.INVALID_INPUT # 断言错误码正确
assert "Input must be an integer" in err_info_py['message'] # 断言错误消息正确

print("
Python: Testing service call simulating resource unavailable...")
s, err_info_py2 = lua_service_caller(-5) # 模拟资源不可用
assert s is False # 断言失败
assert err_info_py2['code'] == PythonErrorCodes.RESOURCE_UNAVAILABLE # 断言错误码正确

代码解释:

PythonErrorCodes: Python类,用作错误代码的枚举。
python_service_with_error_codes(): Python函数,它不抛出异常,而是返回一个布尔成功标志和包含结果数据或错误信息的表(字典)。错误信息表包含codemessage字段。
lua_script_handling_custom_errors: Lua脚本定义了对应的错误代码(可选,也可以直接用Python传来的值),并根据py_service返回的成功标志和错误代码来执行不同的逻辑。
这种方式使得错误信息更加结构化,便于双方进行程序化的错误处理。

11.6 lupa 特定的错误处理机制

lupa.LuaError 的属性: 虽然lupa.LuaError主要通过其字符串表示形式传递信息,但值得检查它是否暴露了其他属性(例如,在某些版本的lupa或特定错误情况下)。通常,它直接继承自RuntimeError,没有太多额外属性,错误信息主要在消息字符串中。
配置选项: lupa.LuaRuntime的构造函数或lupa的其他部分是否有影响错误报告的配置选项?(根据当前lupa的文档,似乎没有太多直接控制错误格式化或传播的特定高级选项,它主要依赖于标准的Lua错误机制和Python异常的转换)。
迭代器和生成器的错误: 当Lua代码迭代由Python提供的迭代器或生成器时,如果Python的迭代器/生成器中途抛出异常,这个异常通常也会被lupa转换为LuaError,从而中止Lua的迭代。

import lupa
from lupa import LuaRuntime

lua = LuaRuntime() # 创建Lua运行时实例

def py_generator_with_error():
    print("Python: Generator started") # 打印生成器开始信息
    yield 1 # 产生第一个值
    yield 2 # 产生第二个值
    raise ValueError("Error inside Python generator") # 在生成器中抛出异常
    # yield 3 # 这行不会被执行

lua.globals().py_gen = py_generator_with_error # 将生成器注册到Lua

lua_script_iterating_generator = """
print("Lua: Iterating Python generator py_gen()")
local count = 0
-- 使用 pcall 来安全地迭代,因为生成器可能在迭代中途出错
local status, err = pcall(function()
    for val in py_gen() do -- 迭代Python生成器
        print("Lua: Received from generator:", val) -- 打印接收到的值
        count = count + 1
        if count >= 10 then error("Lua: Too many iterations, stopping.") end -- 安全措施,防止无限循环
    end
end)

if not status then
    print("Lua: Error during iteration (caught by pcall):", err) -- 打印迭代错误
else
    print("Lua: Iteration completed successfully (or generator exhausted cleanly). Count:", count) -- 打印迭代成功信息
end
return count, err -- 返回迭代计数和错误(如果有)
"""

print("Python: Executing Lua script that iterates a failing Python generator...")
try:
    count, err_msg = lua.execute(lua_script_iterating_generator) # 执行Lua脚本
    print(f"Python: Lua script finished. Count={
              count}, Error='{
              err_msg}'") # 打印结果
    if err_msg: # 如果有错误信息
        assert "Error inside Python generator" in str(err_msg) # 断言错误信息包含预期内容
except lupa.LuaError as e: # 万一pcall没有完全捕捉或配置不同,也准备好处理LuaError
    print(f"Python: Caught LuaError during Lua execution: {
              e}") # 打印捕获到的LuaError
    assert "Error inside Python generator" in str(e) # 断言错误信息包含预期内容

代码解释:

py_generator_with_error(): 一个Python生成器,在产生几个值之后会抛出ValueError
lua_script_iterating_generator: Lua脚本使用for val in py_gen() do ... end的泛型for循环来迭代这个Python生成器。
重要: 整个for循环被包裹在一个pcall中。这是因为当Python生成器py_gen在迭代过程中(即在__next__()方法中)抛出异常时,lupa会将这个异常转换为Lua错误。如果没有pcall,这个Lua错误会中止整个Lua脚本,并可能以lupa.LuaError的形式传播回Python。
pcall捕获到错误后,Lua脚本可以优雅地报告这个问题,而不是直接崩溃。
Python端检查返回的err_msg,它应该包含来自Python生成器的错误信息。

处理跨语言错误和调试需要耐心和系统的方法。利用好两种语言各自的调试工具,结合清晰的接口设计和详尽的日志记录,是成功管理lupa应用程序复杂性的关键。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容