【问题标题】:Pointer from Rust to C via bindgen: first element is always zero通过 bindgen 从 Rust 指向 C 的指针:第一个元素始终为零
【发布时间】:2019-09-30 10:26:00
【问题描述】:

我使用 bindgen 为我的 Rust 代码生成 C 接口。我想将包含 Option<Vec<f64>> 的结构从 Rust 返回到 C。在Rust 我创建了以下结构:

#[repr(C)]
pub struct mariettaSolverStatus {
    lagrange: *const c_double
}

哪个 bindgen 翻译成下面的 C 结构:

/* Auto-generated structure */
typedef struct {
  const double *lagrange;
} mariettaSolverStatus;

Rust中对应的结构是

pub struct AlmOptimizerStatus {
    lagrange_multipliers: Option<Vec<f64>>,
}

impl AlmOptimizerStatus {

    pub fn lagrange_multipliers(&self) -> &Option<Vec<f64>> {
        &self.lagrange_multipliers
    }

}

这个想法是将AlmOptimizerStatus(在Rust中)映射到mariettaSolverStatus(在C中)。当lagrange_multipliersNone 时,C 中的指针会被分配一个空指针。

现在在 Rust 中,我有以下功能:

#[no_mangle]
pub extern "C" fn marietta_solve(
    instance: *mut mariettaCache,
    u: *mut c_double,
    params: *const c_double
) -> mariettaSolverStatus {

  /* obtain an instance of `AlmOptimizerStatus`, which contains
   *  an instance of `&Option<Vec<f64>>` 
   */
  let status = solve(params, &mut instance.cache, u, 0, 0);

  /* At this point, if we print status.langrange_multipliers() we get 
   *
   *  Some([-14.079295698854809,
   *         12.321753192707693,
   *         2.5355683425384417
   *       ])
   *
   */

  /* return an instance of `mariettaSolverStatus` */
  mariettaSolverStatus {
    lagrange: match &status.lagrange_multipliers() {
        /* cast status.lagrange_multipliers() as a `*const c_double`,
         * i.e., get a constant pointer to the data
         */
        Some(y) => {y.as_ptr() as *const c_double},
        /* return NULL, otherwise */
        None => {0 as *const c_double},
    }
  }
}

Bindgen 生成一个 C 头文件和库文件,允许我们在 C 中调用 Rust 函数。到目前为止,我应该说我没有收到来自 Rust 的警告。

但是,当我从 C 调用上述函数时,使用自动生成的 C 接口,mariettaSolverStatus.lagrange第一个元素 始终是 0,而所有后续元素都正确存储.

这是我的 C 代码:

#include <stdio.h>
#include "marietta_bindings.h"

int main() {
    int i;
    double p[MARIETTA_NUM_PARAMETERS] = {2.0, 10.0};  /* parameters    */
    double u[MARIETTA_NUM_DECISION_VARIABLES] = {0};  /* initial guess */
    double init_penalty = 10.0;
    double y[MARIETTA_N1] = {0.0};

    /* obtain cache */
    mariettaCache *cache = marietta_new();

    /* solve  */
    mariettaSolverStatus status = marietta_solve(cache, u, p, y, &init_penalty);

    /* prints:
     * y[0] = 0  <------- WRONG!
     * y[1] = 12.3218
     * y[2] = 2.5356
     */
    for (i = 0; i < MARIETTA_N1; ++i) {
        printf("y[%d] = %g\n", i, status.lagrange[i]);
    }


    /* free memory */
    marietta_free(cache);

    return 0;
}

我猜想在某个地方,某个指针超出了范围。

【问题讨论】:

标签: c rust ffi


【解决方案1】:

我很确定问题在于您对marietta_solve 的实施。让我们逐行浏览

let status = solve(params, &mut instance.cache, u, 0, 0);

您已分配AlmOptimizerStatus 及其所有内部成员。到这里为止,一切都是 kosher(假设 solve 不会做傻事)

mariettaSolverStatus {
  lagrange: match &status.lagrange_multipliers() {
    /* cast status.lagrange_multipliers() as a `*const c_double`,
     * i.e., get a constant pointer to the data
     */
    Some(y) => {y.as_ptr() as *const c_double},
    /* return NULL, otherwise */
    None => {0 as *const c_double},
  }
}

然后您决定将原始指针返回到即将超出范围并被删除的struct (status)。在里面,你有 Option&lt;Vec&lt;f64&gt;&gt; 你正在返回一个指针。

因此,这会导致 UB - 您的向量不再在内存中,但您有一个指向它的原始指针。而且,由于 rust 在使用原始指针时不会保护您免受这种情况的影响,因此不会出现错误。当您分配其他内容时(就像您在定义 int i 时所做的那样),您可能会覆盖您之前使用(和释放)的一些内存。

您可以通过 playground example 说服自己,我已将原始指针替换为引用以触发借用检查器。

为了摆脱这个问题,你需要强制让 Rust 忘记向量的存在,就像这样 (playground):

impl AlmOptimizerStatus {

    pub fn lagrange_multipliers(self) -> Vec<f64> {
        self.lagrange_multipliers.unwrap_or(vec![])
    }

}
fn test() -> *const c_double {

   let status = solve();

   let output = status.lagrange_multipliers();
   let ptr = output.as_ptr();
   std::mem::forget(output);
   ptr
}

注意变化:

  • lagrange_multipliers() 现在解构您的 struct 并采用内部向量。如果您不想要这个,则需要制作一份副本。由于这不是问题的目的,因此我使用解构来降低代码
  • std::mem::forget 忘记一个 rust 对象,允许它超出范围而不被释放。这就是您通常通过 FFI 边界传递对象的方式,第二种选择是通过 MaybeUninitstd::ptr 或其他方式分配内存。

而且显而易见的问题是:在不处理我们在 C 端(通过 free)或 rust 端(通过重新组合 Vec 然后正确删除它)创建的内存泄漏的情况下这样做,显然会, 内存泄漏

【讨论】:

  • forget Vec 不允许您恢复它,除非您保存它的容量。如果只保存ptrlength,需要先做shrink_to_fit保证没有多余的容量,再做Vec::from_raw_parts(ptr, len, len)恢复。或者,我的首选解决方案是使用into_boxed_slice + Box::into_raw,然后使用Box::from_raw 来恢复并释放它。
  • @trentcl 非常值得添加。您介意我扩展答案中的第二个选项(当然要注明出处)吗?
  • @SébastienRenauld 非常感谢您的回答。我宁愿不改变lagrange_multipliers,因为很多功能都依赖于它借用self。它返回Option 而不是Vec 也很重要。上面的代码,除了AlmOptimizerStatus,其实是自动生成的,所以我知道lagrange_multipliers的大小。作为一种解决方法,我目前将langrange 定义为一个固定大小的数组([c_double; N1]),然后将数据从status.lagrange_multipliers() 复制到lagrange
  • @PantelisSopasakis 如果您在返回后不立即删除struct,这两点都可以。这是你的核心问题——你在使用指针之前放弃了status,以及为什么我建议不要借用self(你的MCVE对它的使用为零)。您需要为此找到解决方案,无论是将结构保留为另一个结构的成员,还是复制Vec
  • 当然,请做!我不要求对不请自来的建议进行归因,但做你认为合适的事情。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-21
相关资源
最近更新 更多