ARTICLE · 996451
摩尔格网:手把手教你写一个格网插件
从混乱到统一,SDGAL如何重塑时空格网处理范式
章一天,公众号:章一天摩尔格网:从混乱到统一,SDGAL如何重塑时空格网处理范式

SDGAL 里插件分三种:网格驱动插件、数据源插件、功能插件。开发套路都差不多(建目录 → 写头文件 → 写实现 → 写 CMake → 挂进主工程 → 编译部署 → 测试),咱们先拿最典型的网格驱动插件过一遍全流程,最后再唠唠另外两种插件的区别,以及大家最常遇到的场景——怎么把一个现成的第三方编码库"塞"进 SDGAL。

一、开工之前:不用啃核心源码,一份 SDK 就够了
先说个好消息:写SDGAL格网插件根本不需要核心库的源码。
官方提供的开发包(`3RD/sdgal` 目录),里只有三样东西:头文件、Windows 下的 `sdgalcore.lib` 导入库、还有真正跑起来要用的 `sdgalcore.dll`(Linux 是 `.so`)。压根没有 `.cpp` 文件。就连官方自己的示例——`Driver/GeoHash` 驱动示例、`Plugin/Test` 插件示例,也是拿着这套二进制 SDK 从头写出来的,从没碰过核心库的源码。
联系电话:0371-55019103,电子邮箱:jx@jx-lab.com
不过话说回来,"二进制分发"这事儿也有代价,就是必须保证 ABI 一致,不然轻则加载不上,重则直接崩给你看。开发前记住几条:
Debug 配 Debug,Release 配 Release,千万别混着来;
C++ 标准(这里是 C++11)和运行时库链接方式得跟核心库对齐;
所有插件都要动态链接同一个 `sdgalcore.dll`,各插件自己再单独链一份是不行的,不然反射机制认不出你的类型;
`D_PLUGIN_REG` 注册宏最后那个版本号参数(比如 `"0.9.x"`),声明的是这个插件依赖的核心版本,跨大版本不保证兼容;
如果插件要用到 GDAL、PROJ 这类第三方库,SDK 自带的运行时直接拿来用就行,不用自己重新编译一遍。
环境要求也不高:Windows 7+ 或者 Ubuntu 18.04+,CMake 3.16 以上,一台普通电脑就能开工。
第一步:先把目录建起来

SDGAL 仓库的骨架是固定的:
3RD/sdgal/ # SDK,只读,别动它App/ # 用插件的应用程序Driver/ # 网格驱动插件都放这儿Plugin/ # 数据源、功能插件放这儿Out/bin/<Config>/├── drivers/ # 编译出的驱动会自动跑到这└── plugins/ # 编译出的其他插件会自动跑到这
想写一个新驱动,就在 `Driver/` 下面新建个文件夹,名字就叫你的驱动名(比如 `Driver/MyDriver/`),里面照例分 `include/` 和 `src/`,再加个 `CMakeLists.txt`:
Driver/MyDriver/├── include/mydriver.h├── src/mydriver.cpp└── CMakeLists.txt
就这么简单,先把架子搭起来。
第二步:写头文件——先想清楚"我这网格是几维的"
开发一个网格驱动,说白了就是**写一个继承 `IDriver` 的类,再根据需要拼上 `IDriver2D/3D/4D` 这些维度接口**。这一步最关键的是先想明白:我这个网格编码是平面的还是立体的?编码本身是数字还是字符串?
对照着选就行:
只有平面 → `IDriver + IDriver2D`(最简单的组合,GeoSOT 2D 就这么写的)
三维、高度和平面统一处理 → `IDriver + IDriver3D`
三维、高度单独分层 → `IDriver + IDriver3DS`
四维、平面/高度/时间都分开 → `IDriver + IDriver4DZTS`,再加个 `IDriverExt` 拿到更多高级能力
这些维度接口彼此谁也不继承谁,所以随便怎么组合都不会撞车(专门设计成这样,就是为了避免 C++ 里那个闹心的菱形继承问题)。
好消息是,`IDriver` 里那些方法虽然全是纯虚函数、必须一个不落全部实现,但 `IDriver2D` 这类维度接口其实只有 `level()` 一个方法是非实现不可的,其余都有默认实现兜底。所以别慌,先把最小的一套跑通,剩下的高级功能可以慢慢补。
头文件大概长这样:
#include"driver/driver2d.h"#include"driver/driverext.h"#include"driver/driveri.h"namespace SDGAL {class MyDriver : public IDriver, public IDriverExt, public IDriver2D{D_META(IDriver, IDriverExt, IDriver2D) // 这行别漏,写少了后面转类型会失败public:explicitMyDriver(Object* parent = nullptr);virtual ~MyDriver();virtual DriverInfo info()constoverride;virtualboolisValid(const Code& code)override;virtual Coordinate codeToCenter(const Code& code)override;// ... 其他 IDriver 方法virtual uint32 level(const Code& code)override;virtual Code pointToCode(uint32 level, const Coordinate& point)override;};}
`D_META(...)` 这一行特别容易被忽略,但它其实是反射机制能不能生效的命门——括号里得把这个类实际继承、且需要参与多态转换的基类都列全,少写一个,后面用 `D_OBJECT_CAST` 转类型的时候就会莫名其妙拿到 `nullptr`,排查起来很折磨。
第三步:填实现——顺带完成"报户口"
实现文件里,最容易被遗漏但绝对不能漏的一行,是插件注册宏,得写在 `.cpp` 的全局作用域里(可以在 namespace 外面也可以在里面,但不能塞进某个函数体内):
D_PLUGIN_REG(SDGAL::MyDriver, "MyDriver", "1.0", "我的自定义驱动", "0.9.1")这行代码就相当于给你的驱动"上户口"——没有它,`dCore` 扫描的时候压根不知道你这个插件的存在。
然后是 `info()` 方法,这个要如实填:你这个网格支持几层、精度范围是多少、单位是什么,因为上层应用要靠这些信息判断能不能用你这个驱动,可别偷懒抄别的驱动的数值。
DriverInfo MyDriver::info() const {DriverInfo info;info.capability.supports2D = true;DriverMetadata<STUnit, double> horizontal;horizontal.levelRange = {1, 31};horizontal.unit = STUnit::DEGREE;horizontal.precisionRange = {0.001, 1000.0};info.horizontal = horizontal;return info;}
剩下的活儿,就是把你脑子里那套网格编码的数学模型(不管是立方球面投影、Morton 编码、还是别的什么骚操作),翻译成 SDGAL 认识的几个标准类型(`Code`、`Coordinate`、`BoundingBox`)之间的转换函数。老实说,这部分才是真正的工作量所在,架构层面帮你把"怎么接得上"都想好了,剩下就是把你的算法本体填进去。
调试的时候,用框架自带的日志宏(`DERR`/`DINF`/`DWAR`)会比自己 `printf` 舒服很多,格式统一,还能顺手记录哪里出了问题:
if (!isValid(code)) {DERR("Invalid code");return Code();}
第四步:CMakeLists.txt,照抄就行
这一步基本没什么可发挥的,套模板:
project(mydriver VERSION ${SDGAL_VERSION} LANGUAGES C CXX)file(GLOB_RECURSE SOURCE_MAIN CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)add_library(${PROJECT_NAME} SHARED ${SOURCE_MAIN})target_include_directories(${PROJECT_NAME}PUBLIC$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>${LIBSDGAL_INCLUDE_3RD_DIR}${LIBSDGAL_INCLUDE_DIR})target_link_libraries(${PROJECT_NAME} PRIVATE ${LIBSDGAL_LIBRARY})install(TARGETS ${PROJECT_NAME}RUNTIME DESTINATION binLIBRARY DESTINATION lib)
两个小提醒:一是输出路径不用你操心,`Driver/CMakeLists.txt` 已经统一设置好会落到 `Out/bin/<Config>/drivers`;二是这份 CMake 在 Windows 和 Linux 下写法完全一样,不用你自己判断平台,这些差异早就在根目录的 CMakeLists 里处理干净了。
第五步:让主工程"看见"你
最后一步,去 `Driver/CMakeLists.txt` 里加一行:
add_subdirectory(MyDriver)搞定。数据源插件、功能插件同理,加进 `Plugin/CMakeLists.txt`。
编译、部署,跑起来看看
Windows
mkdir Build && cd Buildcmake -G "Visual Studio 16 2019" -A x64 -DCMAKE_BUILD_TYPE=Debug ..cmake --build . --config Debug
Linux
mkdir Build && cd Buildcmake -DCMAKE_BUILD_TYPE=Debug -G Ninja ..ninja -j4
编译完了驱动会自动落到 `Out/bin/Debug/drivers/` 下面。部署的时候记得把整套目录结构原样打包(`sdgalcore.dll` 和其它依赖放 `bin/` 根目录,驱动进 `drivers/`,插件进 `plugins/`),Windows 把这个目录加进 `PATH`,Linux 加进 `LD_LIBRARY_PATH`,不然运行时互相找不到人。
应用层用起来也不复杂,先扫描注册,再按名字取:
dCore->setDriverPath(exepwd + "/drivers");dCore->setPluginPath(exepwd + "/plugins");dCore->registerAll();// 注意:这里传的是驱动文件名,不是驱动名称SDGAL::IPlugin* plugin = dCore->getDriver("mydriver");SDGAL::IDriver2D* driver2d = D_OBJECT_CAST(SDGAL::IDriver2D*, plugin);if (driver2d) {SDGAL::Code code = driver2d->pointToCode(10, {116.39, 39.90});// 转出来的编码就能用了}
想验证自己写的对不对,可以按这个顺序过一遍:先看驱动有没有被扫描到(打印一下 `dCore->getAllDrivers()`),再试试点转编码、编码转回坐标,来回转一圈看能不能对上;如果是数据源插件,就再跑一次 `load()`。
数据源插件、功能插件:换个接口,套路不变
网格驱动这条主线走完了,另外两种插件其实是"抄同一份作业,换换接口"。
数据源插件不管网格编码,管的是矢量数据怎么读怎么写。核心是实现 `IGeomDriver` 接口——`name()`、`extension()`、`load()`、`save()` 这些必须实现,其余可选。插件本身只负责在 `initialize()` 里把这个 `IGeomDriver` 注册进全局表:
bool MyDataSource::initialize() {GDR().registerDriver(mDataDriver); // 会自动按 extension() 建索引return true;}
功能插件更纯粹,就是暴露一组和网格、数据读写都不沾边的业务能力,比如内置的坐标投影插件 `dproj`。写法上也就是继承 `IPlugin`、按需覆盖 `initialize()`/`shutdown()`,CMakeLists 跟前面几乎一模一样。
加分题:接一个现成的第三方编码库进来
比"从零写算法"更常见的情况是:手上已经有一套第三方编码 SDK 了,只需要写个"适配器"把它接进 SDGAL 就行。这里分两种情况,取决于第三方给你的是什么。
情况一:只给了编译好的 lib/dll,没有源码。那你的适配器类里就得做一层"翻译"——把 SDGAL 的 `Code` 和第三方那套原生编码(比如字符串编码,直接用 SDGAL 自带的 `CodeStr` 存就行,不用自己再设计一套打包方案)来回转换,其余的方法全部转手调用第三方的 API:
Coordinate MHT4063Driver::codeToCenter(const Code& code) {std::string native;toNativeCode(code, native);double x, y, z;MHT4063_CodeToCenter(native.c_str(), &x, &y, &z);return Coordinate(x, y, z);}
这种接法要多留意几个坑:第三方的 `std::string`、自定义结构体这些类型,千万别让它们跑到 SDGAL 对外的接口签名上,只在适配器内部消化掉就行,不然两个库之间容易闹 ABI 别扭;另外第三方的运行时 dll 得跟着驱动一起部署,编译完记得拷到部署目录(但别拷进 `drivers/` 文件夹,那儿要保持干净,只放驱动本体)。
情况二:第三方给了完整源码。这条路明显轻松不少——把源码原样丢进你驱动工程的 `src/` 子目录,CMake 里用两个 `GLOB_RECURSE` 把第三方源码和你的适配器代码一起编进同一个共享库:
file(GLOB_RECURSE MHT4063_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/mht4063/*.cpp)file(GLOB_RECURSE SOURCE_MAIN ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)add_library(${PROJECT_NAME} SHARED ${SOURCE_MAIN} ${MHT4063_SOURCES})
好处很直接:不用 `find_library`,不用额外部署运行时依赖,也不用担心 ABI 打架——反正最后就出一个 `.dll`,第三方算法和 SDGAL 适配层一起打包进去了。而且因为是源码级集成,你可以直接用第三方原生的 C++ 接口,不用像上面那种情况一样被收窄成 C 风格的 `char*`/`bool`。官方 `Driver/GeoHash` 示例走的就是这条路,GeoHash 的算法源码原封不动放在 `src/geohash/` 下,适配器只是薄薄一层转发。
所以要是第三方两种交付形式都有,闭着眼睛选源码接入就对了,省心还没风险。
踩坑自查清单
写完插件跑不起来,八成是这几个原因之一:
驱动/插件加载不出来 → 先看文件是不是放对目录了(驱动进 `drivers/`,其余进 `plugins/`),再看 `main()` 里是不是先设了路径再调用 `registerAll()`,顺序反了不生效;
`D_OBJECT_CAST` 老是转出 `nullptr` → 大概率是 `D_META(...)` 里基类列表没写全,或者 `D_PLUGIN_REG` 不小心塞进了 namespace 里面(必须在全局作用域);
提示找不到你的驱动 → 检查注册宏里写的名字和你查询用的名字是不是一个字都没错,再确认真的调用了 `registerAll()`;
内存泄漏 → 记得创建对象时指定 parent,让对象树帮你管;没指定 parent 的顶层对象记得自己 delete;也别嫌麻烦把 `dCore` 设成父对象,那样这块内存会跟着 `dCore` 一直占着不放;
Windows 编译过、Linux 编译不过 → 多半是自己手写了 `\` 路径分隔符,或者手动区分了平台去写库文件后缀,这些其实根目录的 CMake 早就统一处理好了,插件层不用自己折腾。
这套流程走一遍熟悉了之后,你会发现给 SDGAL 接一个新网格、新数据源、新功能,真的更像是"填空"而不是"重新设计"——架构负责把路铺好,你只要把自己的算法翻译成几个标准接口方法就行。

摩尔格网(MGC, Moore Grid Code)
一种面向未来的四维时空格网编码体系
让地球”像素化”,让时空“可计算”