我使用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_multipliers
为None
时,会将空指针分配给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;
}
我想,某种程度上,某些指针超出了范围。
我很确定问题出在marietta_solve
的实现中。让我们逐行浏览
let status = solve(params, &mut instance.cache, u, 0, 0);
您已经分配了一个AlmOptimizerStatus
及其所有内部成员。到这里为止,一切都是洁净的(假设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<Vec<f64>>
。
结果是,这导致了UB-您的向量不再在内存中,但是您有指向它的原始指针。并且,由于使用原始指针时,锈不会保护您免受此侵害,因此不会出现错误。当您分配其他内容时(如定义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
forgetsMaybeUninit
,std::ptr
或其他方式分配内存。和明显的陷阱:在不处理我们在C侧(通过free
)或生锈侧(通过重新组合Vec
然后适当地将其丢弃)创建的内存泄漏的情况下,这样做显然会,泄漏内存