resource_retriever 源码详细分析

resource_retriever 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/resource_retriever
版本:3.1.3resource_retriever 包),许可证 BSD

resource_retriever 是一个极小的资源加载库:把多种 URI 协议(package://file://http://ftp:// 等)统一解析,并将文件内容读入内存。最初为 rviz 加载 mesh/纹理 设计,现仍是 ROS 2 可视化栈中 package:// 的底层实现之一。


1. 仓库结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
resource_retriever/
├── resource_retriever/ # 主包(C++ 库 + Python 模块)
│ ├── include/resource_retriever/
│ │ ├── retriever.hpp # 公共 API
│ │ └── visibility_control.hpp # 符号导出宏
│ ├── src/
│ │ ├── retriever.cpp # C++ 实现(~145 行)
│ │ └── resource_retriever/
│ │ └── __init__.py # Python API
│ ├── test/
│ │ ├── test.cpp # gtest
│ │ ├── test.py # pytest
│ │ └── test.txt # 测试用 package 资源
│ └── CMakeLists.txt
└── libcurl_vendor/ # libcurl 依赖封装(vendor 或系统库)
├── CMakeLists.txt
├── libcurl_vendor-extras.cmake.in
└── env_hook/ # LD_LIBRARY_PATH / PATH 钩子

特点:核心逻辑不到 200 行;C++ 与 Python 两套 API 语义一致,但实现独立。


2. 架构总览

URI 输入resource_retriever主要消费者转为 file://package://pkg/pathfile:///pathhttp://...ftp://...package:// 解析ament_index_cpplibcurl easy handleMemoryResourcerviz mesh/纹理/图标visualization_msgs Marker
组件 职责
C++ Retriever package:// 解析 + libcurl 下载/读文件
Python get() 同样解析 package://,用 urllib 读取
libcurl_vendor 系统无 curl 时从源码构建 7.81.0 并安装到 opt/libcurl_vendor
ament_index package://foo/... 映射到 share/foo/... 绝对路径

3. 公共 API

3.1 C++:RetrieverMemoryResource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct MemoryResource
{
MemoryResource()
: size(0)
{
}

std::shared_ptr<uint8_t> data;
size_t size;
};

class RESOURCE_RETRIEVER_PUBLIC Retriever
{
public:
Retriever();
~Retriever();
MemoryResource get(const std::string & url);
private:
Retriever(const Retriever & ret) = delete;
CURL * curl_handle_;
};
类型/方法 说明
MemoryResource shared_ptr<uint8_t> + size,二进制缓冲区
Retriever::get(url) 同步获取资源,失败抛 resource_retriever::Exception
Exception 继承 std::runtime_error,格式:Error retrieving file [url]: msg

头文件中 CURL 仅前向声明,避免在公共头里 #include <curl/curl.h>

3.2 Python

1
2
3
4
5
6
7
8
9
10
def get_filename(url, use_protocol=True):
# package:// → file:// 或纯路径
...

def get(url):
filename = get_filename(url)
try:
return urlopen(filename).read()
except URLError:
raise Exception('Invalid URL: {}'.format(filename))
函数 返回值 说明
get_filename(url) str 仅做 URI 转换,不读文件
get(url) bytes 读入内存

Windows 上对 file:// 路径做了特殊处理(反斜杠、/ 前缀)。


4. C++ 核心实现:Retriever::get

4.1 libcurl 全局初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CURLStaticInit
{
public:
CURLStaticInit()
{
CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
...
}
~CURLStaticInit()
{
if (initialized_) {
curl_global_cleanup();
}
}
};
static CURLStaticInit g_curl_init;

进程内静态对象保证 curl_global_init / curl_global_cleanup 配对。每个 Retriever 实例持有独立的 curl_easy_init() handle,便于复用连接(注释说明多次访问同一 URL 可保持连接)。

4.2 package:// 解析

1
2
3
4
5
6
7
8
9
10
11
12
MemoryResource Retriever::get(const std::string & url)
{
std::string mod_url = url;
if (url.find("package://") == 0) {
mod_url.erase(0, strlen("package://"));
size_t pos = mod_url.find("/");
...
std::string package = mod_url.substr(0, pos);
...
package_path = ament_index_cpp::get_package_share_directory(package);
mod_url = "file://" + package_path + mod_url;
}

解析规则:

1
2
3
package://<package_name>/<relative_path>

file://<share_dir>/<relative_path>

例如:

1
2
package://resource_retriever/test/test.txt
→ file:///opt/ros/humble/share/resource_retriever/test/test.txt

校验点:

  • 必须有 / 分隔包名与路径
  • 包名不能为空(package:///test.xml 会抛异常)
  • 包不存在 → PackageNotFoundError 被捕获并转为 Exception

4.3 libcurl 读入内存

1
2
3
4
5
6
7
8
9
10
11
curl_easy_setopt(curl_handle_, CURLOPT_URL, mod_url.c_str());
curl_easy_setopt(curl_handle_, CURLOPT_WRITEFUNCTION, curlWriteFunc);
...
CURLcode ret = curl_easy_perform(curl_handle_);
if (ret != 0) {
throw Exception(mod_url, error_buffer);
} else if (!buf.v.empty()) {
res.size = buf.v.size();
res.data.reset(new uint8_t[res.size], std::default_delete<uint8_t[]>());
memcpy(res.data.get(), &buf.v[0], res.size);
}

流程:

  1. curlWriteFunc 把数据追加到 std::vector<uint8_t>
  2. 成功后拷贝到 shared_ptr<uint8_t[]>(从 ROS 1 时代 boost::shared_array 迁移而来)
  3. 空文件时 size=0data 为 null

未设置的超时、重定向限制、SSL 校验等 curl 选项均使用 libcurl 默认值。


5. libcurl_vendor 依赖层

resource_retriever 不直接依赖系统 curl,而是通过 libcurl_vendor

1
2
3
4
5
6
7
find_package(CURL QUIET)

if(FORCE_BUILD_VENDOR_PKG OR NOT CURL_FOUND)
build_libcurl() # ExternalProject 下载 curl-7.81.0
ament_environment_hooks(...) # 设置 LD_LIBRARY_PATH / PATH
set(CURL_FOUND FALSE)
endif()

libcurl_vendor-extras.cmake.in 逻辑:

  1. 优先 find_package(CURL)
  2. 失败则用 vendor 构建产物(Linux 走 pkg-config libcurl,Windows 走 CURL::libcurl target)
  3. 导出 libcurl_vendor_LIBRARIESINCLUDE_DIRS 等供下游链接

CMakeLists 中有 TODO:未来可能去掉 vendor,直接使用系统 curl。


6. 支持的 URI 协议

协议 C++ Python 说明
package:// ✅ 先转 file:// ✅ 同左 ROS 包 share 目录资源
file:// ✅ libcurl ✅ urllib 本地文件
http:// / https:// ✅ libcurl ✅ urllib 网络资源
ftp:// ✅ libcurl ✅ urllib 理论支持,测试较少

不支持embedded://(rviz Marker 专用,由 rviz 层处理,不经过 resource_retriever)。


7. ROS 2 典型消费者

7.1 rviz:图标与 pixmap

1
2
3
4
5
6
7
8
9
10
11
12
resource_retriever::MemoryResource getResource(const std::string & resource_path)
{
resource_retriever::Retriever retriever;
...
try {
res = retriever.get(resource_path);
} catch (resource_retriever::Exception & e) {
RVIZ_COMMON_LOG_DEBUG(e.what());
return resource_retriever::MemoryResource();
}
return res;
}

loadPixmap("package://rviz_common/icons/...")QPixmap::loadFromData

7.2 rviz:mesh 加载(Ogre / Assimp / STL)

1
2
3
4
5
6
7
8
Ogre::MeshPtr loadMeshFromResource(const std::string & resource_path)
{
...
auto res = getResource(resource_path);
// .mesh → Ogre MeshSerializer
// .stl → STLLoader
// 其他 → AssimpLoader(dae/obj 等)
}

Assimp 自定义 IO 系统直接调用 Retriever::get,使主 mesh 与相对路径纹理/子资源都能走同一套 URI 解析:

1
2
3
4
5
6
7
8
9
10
Assimp::IOStream * Open(const char * file, const char * mode = "rb") override
{
resource_retriever::MemoryResource res;
try {
res = retriever_.get(file);
} catch (const resource_retriever::Exception & e) {
return nullptr;
}
return new ResourceIOStream(res);
}

ResourceIOStreamMemoryResource 上实现 Assimp 的 Read/Seek/Tell,无需落盘临时文件。

7.3 rviz:URDF 纹理

1
2
3
4
5
6
7
8
void RobotLink::loadMaterialFromTexture(...)
{
std::string filename = visual->material->texture_filename;
resource_retriever::Retriever retriever;
res = retriever.get(filename); // 可为 package:// 或 http://
...
Ogre::TextureManager::getSingleton().loadImage(...);
}

7.4 visualization_msgs/Marker

Marker 消息文档明确引用 resource_retriever:

1
2
3
4
5
# Texture resource is a special URI ... acceptable to resource retriever
string texture_resource
...
# mesh_resource uses resource retriever to load a mesh.
string mesh_resource

MESH_RESOURCE 类型 marker 的 mesh_resource 字段常用 package://robot_description/meshes/...


8. 数据流示例

1
2
3
4
5
6
7
8
9
URDF: package://my_robot/meshes/base.dae
↓ robot_state_publisher / rviz 读取
↓ resource_retriever::Retriever::get()
↓ ament_index: my_robot → /install/my_robot/share/my_robot
↓ file:///install/my_robot/share/my_robot/meshes/base.dae
↓ libcurl 读入 MemoryResource
↓ AssimpLoader::ResourceIOSystem::Open()
↓ 解析 dae + 相对路径纹理 package://my_robot/textures/...
↓ Ogre::Mesh → rviz 渲染

9. 测试覆盖

C++ gtest (test/test.cpp)

用例 验证
getByPackage package://resource_retriever/test/test.txt 内容为 'A'
http http://packages.ros.org/ros.key 非空
invalidFiles 无效 file、缺路径 package、不存在包、空包名

Python pytest (test/test.py)

与 C++ 基本对应;差异在于无效包名时 Python 直接抛出 PackageNotFoundError(来自 get_package_share_directory),C++ 则包装为 resource_retriever::Exception


10. 构建与导出

1
2
3
4
5
6
7
add_library(${PROJECT_NAME} src/retriever.cpp)
ament_target_dependencies(${PROJECT_NAME}
ament_index_cpp
libcurl_vendor
)
ament_python_install_package(${PROJECT_NAME} ...)
ament_export_targets(${PROJECT_NAME} HAS_LIBRARY_TARGET)

依赖:

1
2
3
resource_retriever
├── ament_index_cpp / ament_index_python
└── libcurl_vendor → libcurl

11. 设计特点与局限

特点 说明
URI 统一入口 本地包资源与远程 URL 同一 API
零落盘 全内存加载,适合嵌入式/只读环境
轻量 无 ROS 节点、无 rclcpp 依赖
连接复用 Retriever 实例复用 curl easy handle
局限 说明
同步阻塞 get() 阻塞直到完成,无 async API
无缓存 每次 get() 重新读取;rviz 在上层用 QPixmapCache/Ogre 缓存
无 ROS 参数/重映射 不做 topic/service 式重映射
C++/Python 异常不一致 无效包名异常类型不同
每次调用新建 Retriever rviz 多处局部创建,未全局共享 handle
双缓冲拷贝 vector → shared_ptr 数组,多一次 memcpy

12. 与 ament_index 的关系

1
2
3
4
package://foo/bar/baz.stl
│ └─ 相对 share 的路径
└─ ament_index_cpp::get_package_share_directory("foo")
→ $AMENT_PREFIX_PATH/.../share/foo

pluginlib 的 index 机制不同:这里只用 包 share 目录查找,不涉及插件 XML 资源类型。


13. 推荐阅读顺序

  1. API 面retriever.hpp__init__.py
  2. 核心逻辑retriever.cppget()curlWriteFunc
  3. 依赖libcurl_vendor/CMakeLists.txt + libcurl_vendor-extras.cmake.in
  4. 集成示例
    • 简单:rviz_common/load_resource.cpp
    • mesh:rviz_rendering/mesh_loader.cpp
    • Assimp IO:assimp_loader.cppResourceIOSystem
    • URDF 纹理:robot_link.cpp
  5. 测试test/test.cpp 理解边界条件

14. 小结

resource_retriever 是 ROS 2 生态中的小型 URI→内存 适配器package:// 交给 ament_index,其余协议交给 libcurl(C++)或 urllib(Python)。本身不做解析 mesh/图像,只负责按 URI 取字节;rviz、Marker、URDF 可视化在上层解释这些字节。

如果你希望,我可以把本文写入 ros2doc/ros/resource_retriever 源码详细分析.md,或继续分析 AssimpLoader 如何把 MemoryResource 转成 Ogre Mesh 的完整链路。

文章互动

阅读 --

留言

0 条留言

正在加载留言…