Указатель от Rust до C через bindgen: первый элемент всегда равен нулю - PullRequest
1 голос
/ 30 сентября 2019

Я использую bindgen для генерации интерфейса C для моего кода Rust. Я хочу вернуть структуру, которая содержит Option<Vec<f64>> от Rust до C. В Rust я создал следующую структуру:

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

, которая связывает перевод в следующую структуру 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заголовочные и библиотечные файлы, которые позволяют нам вызывать функции Rust в C. До этого момента я должен сказать, что я не получаю предупреждений от 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;
}

Я бы предположил, что каким-то образом где-то указатель выходит из области видимости.

1 Ответ

2 голосов
/ 30 сентября 2019

Я почти уверен, что проблема заключается в вашей реализации 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), вы потенциально можете перезаписать часть памяти, которую вы использовали (и освободили) ранее.

Вы можете убедиться в этом с помощью этого пример игровой площадки , где я заменил необработанные указатели ссылками для запуска средства проверки заимствования.

Чтобы выйти из этой проблемы, вам нужно принудительно заставить Rust забыть о существованиивектор, вот так ( детская площадка ):

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 забывает объект ржавчины, позволяя ему выйти из области видимости, не будучи освобожденным,Именно так вы обычно передаете объекты через границу FFI, вторым вариантом является выделение памяти с помощью MaybeUninit, std::ptr или другими способами.

И очевидное замечание: делать это без работы сУтечка памяти, которую мы создали на стороне C (через free) или на стороне ржавчины (путем рекомбинации Vec и последующего ее правильного удаления), очевидно, приведет к утечке памяти

...