
简介lua-cjson 2.1.0 已编译版是面向 Lua 开发者的预编译 JSON 编解码库基于 C 实现免去源码编译环节可在支持的平台上直接加载使用。预编译包已生成 Windows 动态链接库开发者无需安装编译工具链将文件放入 Lua 的模块路径即可通过 require 加载。库的核心功能涵盖 JSON 编码与解码适用于游戏开发、Web 服务接口、配置文件读写等需要快速处理 JSON 数据的 Lua 项目对不熟悉 C 编译流程或希望快速集成 JSON 能力的开发者尤为友好。压缩包共 50 个文件按内容大致分为四类一是动态链接库及头文件提供底层调用接口二是 Lua 脚本与 C 源码便于阅读和学习实现机制三是大量 JSON 示例数据与测试脚本覆盖常见编码解码场景四是使用说明、许可证、更新日志等文档方便查阅安装与使用注意事项。整套资源约 239KB体积紧凑。已有 987 人学习下载。通过 require 加载 cjson 模块后可调用 encode 与 decode 方法完成 Lua 表与 JSON 字符串之间的转换包内测试用例和示例能帮助开发者验证功能、快速上手。附带的测试脚本覆盖典型 JSON 文本支持基准测试与兼容性验证是 Lua 环境中处理 JSON 数据的高效工具。 做Lua服务端的人迟早会遇到同一道坎手里拿着一份JSON数据要么编码要么解码反正不能靠手拼字符串。我去年接手一个老项目时项目里还在用自写的解析器处理配置接口线上偶发解析失败代码堆了三百多行最后痛下决心换成了编译好的lua-cjson-2.1.0动态库从那天起再没为JSON折腾过。这篇文章把我在接入这个库过程中踩过的坑、对比过的方案、调优过的细节都整理一遍给正准备接手lua-cjson的新手和想在项目里换掉手写解析器的老哥们一个完整的参考。lua-cjson就是个用C实现的Lua模块核心功能是把Lua table和JSON字符串互相转换。2.1.0这个版本在兼容性、稀疏数组处理、嵌套深度限制上都比老版本成熟很多。如果你在用OpenResty、Nginx里跑Lua脚本、游戏服务端、嵌入式设备上的Lua环境或者只是单纯想在个人脚本里不再拼字符串拼到手抽筋这篇都适用。1. 从造轮子到换轮子为什么lua-cjson成了默认答案1.1 手写JSON解析为什么总翻车我接手那个老项目时自写解析器代码长这样正则匹配键值对然后递归处理嵌套遇到转义字符和Unicode再单独补逻辑。单看简单场景没问题可JSON这东西看着人畜无害真抠细节能逼疯人字符串里带反斜杠、嵌套深度十几层、键值对里面又套数组、数组里面又有对象……正则在嵌套结构面前基本失灵递归写深了又怕爆栈。线上某次接口返回一个很长很长的数组解析器直接卡住从此我对自写解析器产生了生理性排斥。1.2 主流Lua JSON库横评我整理过当时在选型表里的几个方案给新入坑的朋友一个直观对比库实现方式优点缺点lua-cjson 2.1.0C模块加载后名字叫cjson性能强内存可控API极简大量项目验证过需编译ABI绑定Lua版本dkjson纯Lua不用编译跨Lua版本直接用性能慢大JSON下差距明显luarapidjsonC绑定性能也很好功能丰富依赖较重编译步骤更复杂自写解析器Lua没有依赖但代价全在维护上正确性靠运气性能靠佛祖最终选lua-cjson还有个很现实的原因项目里要跑几十个实例没有精力在每个环境上折腾源码编译直接用编译好的2.1.0版本so/dll塞进去就能跑省心。还有一个隐性优势lua-cjson直接用C栈和Lua交互没有中间层代理处理一两个G的日志文件时内存占用是可控的。2. 2.1.0版本值不值得追以及已编译背后的ABI陷阱2.1 这个版本更新了什么很多老项目还在用1.x或者hack过的0.x版本其实2.1.0有几个关键改进值得升级。最直观的是对Lua 5.3的整数类型做了专门处理数字解析不再一股脑转double整数字面量在Lua 5.3下能尽量落到integer类型这对ID这种字段尤为重要。其次是提供了稀疏数组控制函数encode_sparse_array数组空洞在JSON和Lua之间那个老大难问题终于有了官方解法。再有就是递归深度可以通过encode_max_depth和decode_max_depth调节特别适合处理不可信的外部输入。2.2 已编译包不能随便拿来就用的原因很多人以为已编译就是下下来require一下就完事结果一跑直接报error loading module cjson。问题多半出在ABI不匹配上。Lua的C模块本质是一个动态库它需要和Lua虚拟机链接到同一套符号。Lua 5.1的API和Lua 5.3的API差异很大5.1编译出来的so拿到5.3环境必然报无底洞一样的undefined symbol: lua_tointeger之类的错。所以拿到任何已编译包第一件事确认三件事Lua解释器版本是多少5.1、5.2还是5.35.4要用fork版或自己调整源码解释器是32位还是64位在Windows上还要注意是MSVC编译还是MinGW编译C运行时不一致很容易出现奇怪的崩溃。Linux下自己编译也很省事我常用的命令是make LUA_VERSION5.3 LUA_INCLUDE_DIR/usr/include/lua5.3Lua 5.1的老环境直接make就行。如果只想改个路径可以看Makefile里的LUA_INCLUDE_DIR和LUA_LIBDIR变量别的不用动。3. 接入已编译包的三个步骤路径、加载、自检3.1 package.cpath是第一个拦路虎Lua里有package.path和package.cpath两个变量前者找.lua脚本后者找.so或.dll动态库。require(cjson)加载cjson时走的是package.cpath不是package.path。很多新手把cjson.so往项目目录一丢就require结果总是module cjson not found就是因为默认cpath里没包含当前目录或者项目Lib目录。我习惯的做法是项目入口文件最前面加上package.cpath ./lib/?.so; .. package.cpath local cjson require cjsonWindows上则是./lib/?.dll;。OpenResty环境一般不用管它已经把cjson.so内置到默认路径里了。单独跑标准Lua时这个配置基本是必写的。3.2 二十秒自检脚本装完先别急着写业务跑一个最小验证脚本能一次性暴露90%的问题local cjson require cjson local ok, res pcall(cjson.decode, {name:lua,score:100,tags:[a,b]}) print(decode ok:, ok) if ok then print(name:, res.name) print(score:, res.score) print(tags count:, #res.tags) end print(encode:, cjson.encode({namelua, score100, oktrue}))能输出expected内容说明环境和模块已经通了。这一步别跳过我见过太多同事花一整晚调试业务代码最后发现是cjson.so加载的都是旧版本或者路径根本没对上。3.3 加载失败的高频原因module cjson not foundcpath路径不对多半是前面说的没加路径。undefined symbol: luaL_setfuncsLua版本不匹配5.1的so在5.3下跑会出现这个重新编译。wrong ELF class: ELFCLASS3232位库放到64位Lua里换对应位数。Windows下找不到指定的模块缺VC运行时或者Lua版本对不上装对应运行库或者换编译工具链。4. 核心API的日常用法和反直觉细节4.1 encode/decode/null的基础语义先看最基础的一组用法local cjson require cjson -- 解码 local obj cjson.decode({name:lua,score:100,active:true}) print(obj.name) -- lua print(obj.score) -- 100 -- 编码 local out cjson.encode({namelua, score100, activetrue}) print(out) -- {active:true,name:lua,score:100}有个特别容易踩的点JSON里的null解码出来是cjson.null一个userdata不是Lua的nil。因为nil在Lua table里有特殊含义——表示键不存在。所以{a:null}解码后obj.a不是nil而是cjson.null判断时要写成obj.a cjson.null。反过来编码时你没法把一个值为nil的字段放进table里因为Lua里{anil}直接就是空表想输出a:null必须显式赋cjson.null。local data {a cjson.null, b keep} print(cjson.encode(data)) -- {a:null,b:keep}4.2 空表是对象还是数组——这个坑能埋一整天Lua的表只有一个类型但JSON世界里数组和对象是两种东西。cjson.encode({})默认输出什么答案是{}也就是对象。这本身没问题可当你有一些业务语义上希望输出空数组[]的字段时就懵了。比如items:[]从上游解析下来再原样编码如果中间代码把空数组处理成了空表对象再encode就变成items:{}前端拿到直接崩溃。标准lua-cjson 2.1.0对空表输出{}没有内置开关OpenResty维护的fork里可以用cjson.empty_array_mt标记空表local cjson require cjson.safe local mt cjson.empty_array_mt local data { items setmetatable({}, mt) } print(cjson.encode(data)) -- {items:[]}标准版没有这个元方法的话最稳妥的办法是业务层做约定需要输出空数组的字段在encode前插入一个假元素encode完再替换掉或者用专门的序列化函数绕过。这些方案都丑但能解决问题。4.3 稀疏数组默认会悄悄变成对象Lua的数组允许空洞比如local arr {1, [3]3}arr[2]是nil。这种稀疏表在JSON里没有直接对应物lua-cjson对它处理策略是默认把稀疏数组编码成对象因为元素下标是数字但又不连续用对象{1:1,3:3}语义上更安全。这个转换一旦发生HTTP接口的字段类型就变了数组变对象前端和下游都很痛苦。如果确定你的稀疏表就是想输出成数组空洞用null填充可以这样cjson.encode_sparse_array(false) -- false表示不把稀疏数组转成对象但2.1.0的默认行为依然是安全的保守策略我的建议是别乱改全局配置尽量在上游把数组处理成连续下标再给lua-cjson别让序列化层替你兜底。4.4 错误处理别裸调decode2.1.0的decode在遇到非法JSON时会抛错误直接中断当前代码。所有外部输入、第三方接口返回、用户上传的配置都必须用pcall包一层local ok, data pcall(cjson.decode, raw_str) if not ok then -- 打日志、降级、或者返回错误 end另外一个小细节字符串带一个UTF-8 BOM头时decode会报错因为BOM不是合法JSON起始字符。先raw_str raw_str:gsub(^%z%z%z, )清掉再走decode。5. 性能、内存与精度JSON处理的三个隐藏瓶颈5.1 encode时保持字段顺序的手段lua-cjson在Lua 5.2环境中编码table时字段顺序默认按表的遍历顺序输出并不是很多人以为的字典序。在Lua 5.1下行为可能不太一样老项目如果对接口字段顺序有强制要求我在实践中是这么处理的local ordered {} ordered[#ordered1] {id, 1} ordered[#ordered1] {name, lua} ordered[#ordered1] {items, {}}然后自己写一个编码函数按插入顺序拼接。标准lua-cjson本身不提供保序API但很多场景不需要纠结JSON对象的顺序本来就不该被依赖。只有对接方真的对顺序有变态要求时才值得上这段代码。5.2 大JSON和超大整数的精度问题回到性能话题。我本地用一段200KB的JSON日志做压力测试lua-cjson解码耗时在几毫秒级别而纯Lua的dkjson要慢一个数量级以上差距在重负载下会非常明显。如果你在网关层或者日志分析管道里跑这个差距直接决定能不能撑住流量。真正阴险的是超大整数。Lua 5.1/5.2里的number是双精度浮点能精确表示的整数上限只有2^53。上游返回一个类似9223372036854775807的雪花IDdecode之后变成浮点数精度直接丢失再encode出来就是另一串数字了。这个坑在告警系统里见过太多次服务端日志里打印出来的ID跟数据库里的对不上查得头皮发麻。缓解思路有几种服务端把这种ID用字符串格式传给LuaLua不参与数字运算。如果坚持要保留JSON里的原始数字文本decode前用gsub把指定字段的数值抽取成字符串。用Lua 5.3配合64位integerlua-cjson 2.1.0对整数支持不错但跨2^63仍可能溢出。5.3 深度限制是为了不被打爆默认情况下lua-cjson允许1000层嵌套解码。这个深度对正常业务足够但对攻击者来说就是压垮内存的杠杆构造几万层嵌套的JSON递归调用本身没问题但C栈溢出会把整个Lua进程带崩。所以外部输入场景上线前调小深度cjson.decode_max_depth(64) cjson.encode_max_depth(64)64层足够覆盖绝大多数业务还能隔离异常数据。在Nginx/OpenResty里跑的话这种保护尤其重要崩一个worker就是一波请求全挂。6. 一次真实的线上排错decode后再encode数据变样了最后分享一个我印象特别深的线上事故基本能串起前面所有知识点。现象是网关层用lua-cjson把上游返回的JSON解析后做了字段过滤再encode给下游结果下游收到的东西结构变了。原始数据是一个数组{items:[{id:1},{id:2}],total:2}过滤后变成{items:{1:{id:1},2:{id:2}},total:2}数组静悄悄变成了对象下游解析逻辑直接懵了。排查链路是这样的先打印type(data.items)是table没问题。再打印#data.items输出0。这就很可疑明明有两个元素。检查next(data.items)发现key确实是1和2但#运算符只数到第一个nil为止。再追根溯源代码里有个函数做了元素剔除直接用data.items[1] nil删了一项数组出现空洞Lua的#只能返回第一个空洞之前的下标数于是整个数组在lua-cjson眼里成了稀疏表按默认策略编码成了对象。修复方式也很简单删除元素改成table.remove(data.items, 1)或者重新构造一个连续下标的表再交给lua-cjson。这次事故让我彻底记住了两点第一Lua数组操作必须保持下标连续置nil删除是最危险的操作第二lua-cjson的稀疏数组策略是一把双刃剑理解它的默认行为比出事后再查文档重要得多。个人经验是项目里凡是引入任何编译好的C模块我都会配套写一个自检脚本把空表、null、中文、超大整数、空字符串这几个典型case全跑一遍花十分钟省下来的是线上一个个不眠夜。lua-cjson 2.1.0已经是个非常成熟的库但它再成熟也只是个工具用对姿势才是关键。本文还有配套的精品资源点击获取