#odbc #api #complete #error #safety #safe #handle

no-std rs-odbc

ODBC 的最小化安全 Rust 实现

2 个不稳定版本

0.2.0 2022年10月9日
0.1.0 2021年8月22日

#5 in #odbc

Apache-2.0 协议

495KB
10K SLoC

RS-ODBC

License: Apache 2.0 Build Crates.io

看起来和感觉像 ODBC 但更安全的 ODBC API 的 Rust 实现

描述

该软件包的主要设计目标是,在可能的情况下提供类型安全,同时使暴露的 API 尽可能接近原始 ODBC API。该软件包防止了 C 代码中固有的大多数安全问题,并将大多数应用程序错误移动到编译时。ODBC 状态转换有限状态机 (FSM) 在 Rust 的类型系统中实现,从而防止了许多无效句柄错误成为编译错误。

为什么选择这个软件包

1. 知名的 API

如果您已经使用过 ODBC API,您会发现在使用此软件包时会感到熟悉,即您不需要再学习另一个 API。使用此软件包,您将获得一个广为人知、高度使用和标准化的 API。对原始 ODBC API 的抽象级别非常低,因此您可以使用原始 ODBC 文档。将现有的 ODBC 应用程序或示例从 C 转换为 Rust 非常简单。

2. 安全的 API

对于大多数应用程序,您将永远不会需要使用原始指针。暴露 ODBC 之上的自定义高级 API 封装器的软件包可能迫使您在需要使用它们不支持的 API 表达的 ODBC 功能时回退到使用原始 API。除非这些软件包是在这个软件包之上构建的,否则这将为您的应用程序引入不必要的风险。

3. 完整的 API

该软件包被设计为完全符合 ODBC 标准,因此不应该有任何低级 ODBC 功能不能通过该软件包表达。但是,可能尚未实现某个特定功能。如果您发现某个功能缺失,请鼓励您打开一个要求该功能的 Issue。

安装

要使用此库,您必须在您的宿主操作系统上安装和配置 ODBC 驱动程序管理器。此库在 Windows 上动态链接到 odbc32.dll,在 Linux 和 OS-X 上链接到 libodbc.so(unixODBC)。要启用原生库的静态链接,请使用 cargo 的 static 功能。

Cargo 功能

static

启用本地库的静态链接。如果启用了静态链接,用户必须定义 RS_ODBC_LINK_SEARCH 环境变量,其中包含此crate将要链接的静态库的路径。对于unixODBC,用户应在此路径下提供 libodbc.alibltdl.aWindows 不支持静态链接

API 差异

  1. ODBC 函数作为句柄的方法或关联函数实现。因此,将句柄标识符(例如 SQL_HANDLE_STMT)作为参数传递变得不再必要
C ODBC 示例 Rust ODBC 示例
SQLHSTMT hstmt = SQL_NULL_HSTMT;
int ret1 = SQLAllocHandle(
    SQL_HANDLE_STMT,
    hdbc,
    &hstmt
);
if (SQL_SUCCEEDED(ret1)) {
    int ret2 = SQLCancelHandle(
        SQL_HANDLE_STMT,
        hstmt
    );
}
let (hstmt, ret1) = SQLHSTMT::SQLAllocHandle(&hdbc);
if SQL_SUCCEEDED(ret1) {
    let hstmt = hstmt.unwrap();
    let ret2 = hstmt.SQLCancelHandle();
}
  1. 大多数ODBC句柄方法按照标准返回 SQLRETURN,但某些会返回一个元组 (Result<<succ_handle_type>, <err_handle_type>>, SQLRETURN)(例如 SQLDriverConnect)。返回句柄使得在 Rust 的类型系统中实现 ODBC 状态转换 FSM 成为可能

  2. 接受指针及其长度的 ODBC 函数使用切片的引用。切片引用防止了应用程序编写者超出分配单元末尾写入/读取的可能性。

C ODBC 示例 Rust ODBC 示例
SQLCHAR catalog_name[] = "rs_odbc";
int ret = SQLSetConnAttr(
    hdbc,
    SQL_ATTR_CURRENT_CATALOG,
    catalog_name,
    SQL_NTS,
);
let catalog_name = "rs_odbc";
let ret = hdbc.SQLSetConnAttr(
    SQL_ATTR_CURRENT_CATALOG,
    catalog_name.as_ref()
);
  1. ODBC 版本在分配环境句柄时定义。通常,这应该是您的 ODBC 应用程序的第一步,但在 Rust 中这由类型系统处理
C ODBC 示例 Rust ODBC 示例
SQLHENV henv = SQL_NULL_HENV;
int ret1 = SQLAllocHandle(
    SQL_HANDLE_ENV,
    &SQL_NULL_HANDLE,
    &henv
);
if (SQL_SUCCEEDED(ret1)) {
    int ret2 = SQLSetEnvAttr(
        henv,
        SQL_ATTR_ODBC_VERSION,
        SQL_OV_ODBC3_80,
        0
    );
}
let (env, ret1) = SQLHENV::SQLAllocHandle(&SQL_NULL_HANDLE);
if (SQL_SUCCEEDED(ret1)) {
    let env: SQLHENV<SQL_OV_ODBC3_80> = env.unwrap();
}
  1. 在作用域结束时自动断开连接和释放句柄

未初始化的变量

当使用 ODBC 函数(如 SQLGetEnvAttr)时,这些函数接受可变引用并将其写入,但驱动程序或 DM 永远不会从它们读取,则不需要初始化这些变量,因为它们将在调用相关 ODBC 函数期间进行初始化。为了避免不必要的初始化,许多通过 Rust 公开的 ODBC 函数允许使用已初始化和未初始化的变量(通过 MaybeUninit

use core::mem::MaybeUninit;
use rs_odbc::api::Allocate;
use rs_odbc::env::{self, SQL_ATTR_CONNECTION_POOLING, SQL_OV_ODBC3_80};
use rs_odbc::handle::{SQLHENV, SQL_NULL_HANDLE};

fn main() {
  let (env, _) = SQLHENV::SQLAllocHandle(&SQL_NULL_HANDLE);
  let env: SQLHENV<SQL_OV_ODBC3_80> = env.unwrap();

  let mut value = env::SQL_CP_ONE_PER_HENV;     // Initialized to default value
  let _ = env.SQLGetEnvAttr(SQL_ATTR_CONNECTION_POOLING, Some(&mut value), None);

  // Confirm value was modified by the driver
  assert_ne!(env::SQL_CP_ONE_PER_HENV, value);

  let mut value = MaybeUninit::uninit();        // Variable is uninitialized
  let _ = env.SQLGetEnvAttr(SQL_ATTR_CONNECTION_POOLING, Some(&mut value), None);

  // Value initialized by the driver
  match unsafe { value.assume_init() } {
      env::SQL_CP_ONE_PER_DRIVER => println!("SQL_CP_ONE_PER_DRIVER"),
      env::SQL_CP_ONE_PER_HENV => println!("SQL_CP_ONE_PER_HENV"),
      env::SQL_CP_DRIVER_AWARE => println!("SQL_CP_DRIVER_AWARE"),
      env::SQL_CP_OFF => println!("SQL_CP_OFF"),

      _ => panic!("Driver returned unknown value"),
  }
}

**强烈不建议使用未初始化的变量**,因为它们的使用通常是一种微优化,对代码的性能没有可测量的影响,并且如果处理不当会引入潜在的不确定行为(如部分初始化的变量)。如果某些 ODBC 函数只能接收未初始化的参数,**建议用户使用 MaybeUninit::newMaybeUninit::zeroed** 来最小化不确定行为的风险。

线程安全

**所有句柄都是 Send**,然而,目前,**只有 SQLHENVSync**,因为跨线程共享其他句柄的引用被认为是反模式。显然,要取消在另一个线程上运行的连接或语句句柄上的函数,必须能够跨线程共享句柄引用。由于**取消操作由 ODBC 标准定义为始终是线程安全的操作**,对于这个特定场景,您可以从原始句柄推导出一个实现 Sync 特性的句柄,如 WeakSQLHSTMTRefSQLHSTMT。以 Ref 前缀的句柄是从原始句柄的引用分配的,而以 Weak 前缀的句柄是从您的原始句柄包装在 Arc 中的对象分配的。

// TODO: Add code example

如果您有需要在不同线程之间共享除SQLHENV之外的处理句柄的使用场景,请提交一个问题,描述您的使用场景。

不安全的API

在某些情况下,无法通过类型系统保证安全性。在这些罕见的情况下,您可以分配UnsafeSQLHSTMTUnsafeSQLHDESC,这些类型实现了额外的非安全API,使得一些语句函数变得不安全。

// TODO:

测试

集成测试使用已经配置了数据库和ODBC驱动的docker环境。

可以通过docker-compose up -d设置测试环境
测试使用docker exec -t rs-odbc sh -lc 'cargo test'执行

  • 使用RUSTFLAGS=-Awarnings来关闭编译器警告,这些警告可能导致编译测试失败

依赖项

~250–690KB
~16K SLoC