Rust: Funciones de Rust que se pueden llamar desde C

Creado en 2 feb. 2012  ·  16Comentarios  ·  Fuente: rust-lang/rust

Ahora tenemos muchos escenarios en los que las personas que crean enlaces quieren poder proporcionar una devolución de llamada que una función de C pueda llamar. La solución actual es escribir una función C nativa que use algunas API internas no especificadas para enviar un mensaje al código Rust. Idealmente, implica no escribir código C.

Aquí hay una solución mínima para crear funciones en Rust que se pueden llamar desde código C. La esencia es: 1) tenemos otro tipo de declaración de función, 2) esta función no se puede llamar desde el código de Rust, 3) su valor se puede tomar como un puntero inseguro opaco, 4) se cuece en la magia de conmutación de pila y se adapta desde el C ABI hasta el Rust ABI.

Declaraciones de funciones de C-to-Rust (corteza):

crust fn callback(a: *whatever) {
}

Obtener un puntero inseguro a una función C ABI:

let callbackptr: *u8 = callback;

También podríamos definir algún tipo específicamente para este propósito.

Implementación del compilador:

La mayoría de las veces es sencilla, pero las trans se ponen feas. En trans, necesitaremos hacer básicamente lo contrario de lo que hacemos con las funciones mod nativas:

  • Genere una función C ABI usando la firma declarada
  • Genere una función shim que tome los argumentos C en una estructura
  • La función C rellena los argumentos en una estructura
  • La función C llama a upcall_call_shim_on_rust_stack con la estructura de argumentos y la dirección de la función shim
  • Genere una función Rust ABI usando la firma declarada
  • La función shim extrae los argumentos de la estructura y llama a la función Rust

Implementación en tiempo de ejecución:

El tiempo de ejecución tiene que cambiar de varias formas para que esto suceda:

  • Un nuevo upcall para volver a la pila de Rust
  • Las tareas deben mantener una pila de contextos Rust y contextos C
  • Necesita una estrategia para lidiar con la falla después de volver a ingresar a la pila de Rust
  • Necesita una estrategia para lidiar con ceder después de volver a ingresar a la pila de Rust

Falla:

No podemos simplemente lanzar una excepción después de volver a ingresar a la pila de Rust porque no hay garantía de que el código nativo pueda desenrollarse con excepciones de C ++. Aparentemente, el lenguaje Go simplemente omitirá todos los marcos nativos en este escenario, filtrando todo a lo largo del camino. Nosotros, en cambio, abortaremos: si el usuario quiere evitar una falla catastrófica, debe usar su devolución de llamada de Rust para enviar un mensaje y regresar de inmediato.

Flexible:

Sin cambios en la forma en que manejamos las pilas de C, no podemos permitir que las funciones de Rust cambien de contexto al programador después de volver a ingresar a la pila de Rust desde el código C. Veo dos soluciones:

1) El rendimiento es diferente después de volver a ingresar a la pila de Rust y simplemente bloquear. Las tareas que quieran hacer esto deben asegurarse de tener su propio planificador (# 1721).
2) En lugar de ejecutar código nativo usando la pila del planificador, las tareas comprobarán las pilas C de un grupo ubicado en cada planificador. Cada vez que una tarea vuelve a entrar en la pila C, verificará si ya tiene una y la reutilizará; de lo contrario, solicitará una nueva al programador. Esto permitiría que el código de Rust siempre se rindiera normalmente sin inmovilizar al programador.

Prefiero la segunda opción.

Véase también # 1508

A-debuginfo A-runtime A-typesystem E-easy

Comentario más útil

Por favor, perdone esta resurrección, pero este problema está vinculado desde una pieza de combinador y y algunos otros sitios, y recientemente un novato me preguntó al respecto, así que estoy notando que, según este problema y cambios posteriores, llamo a Rust desde C es simple :

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}

Todos 16 comentarios

Lamento saltar aquí, pero me gustaría enfatizar que llamar a las funciones de C debe ser rápido, como en _brillante_ rápido. Si quiero escribir un juego en rust usando una biblioteca C como Allegro, SDL u Opengl, esto es esencial. De lo contrario, el juego se ralentizará en el código de renderizado donde hay muchas llamadas C, lo cual es inaceptable. El compilador de lenguaje Go predeterminado con cgo tiene tales problemas.

Por lo tanto, preferiría una solución que sea rápida, aunque puede restringir lo que puede hacer la función en el lado de Rust.

Además, ¿no sería una idea usar "native fn" en lugar de "crust fn" o eso tiene otro significado planeado?

@beoran tienes esto al revés. Estamos hablando de C llamando a Rust. Llamar a funciones C desde Rust ya es bastante rápido (podría hacerse algo más rápido).

OK veo. ¿Hay alguna forma de que pueda ayudar a acelerar las llamadas a C desde el óxido?

@beoran, como dije, es un poco ortogonal a este problema ... pero probablemente lo mejor que podría hacer es hacer un punto de referencia que muestre cómo el rendimiento es inadecuado. :)

De acuerdo, lo haré cuando llegue lo suficientemente lejos en envolver Allegro para comparar la sobrecarga de llamarlo desde rust Rust con llamarlo desde C. Dejaré este problema solo por ahora y abriré uno nuevo una vez que tenga el punto de referencia.

¿Podríamos evitar una de las copias de los argumentos haciendo que la función C escriba directamente en la pila de Rust, como lo hacen actualmente las llamadas a Rust-> C?

Espero que la copia final de los argumentos fuera de la estructura shim y dentro de los argumentos de la función rust se elimine mediante la inserción. Sin embargo, no estoy seguro de si eso es a lo que te refieres.

Creo que nuestras llamadas de C actualmente copian los argumentos en una estructura en la pila de Rust y luego copian esa estructura en la pila de C.

@pcwalton Rust-> C actualmente no escribe directamente en la pila de C porque es específico de i386. Quería evitar tener que escribir código que fuera específico de una convención de llamada en particular, incluso al precio de un poco de rendimiento, para que el funcionamiento de 64 bits funcione. (Creo que tales optimizaciones podrían tener sentido ahora, sin embargo, particularmente porque el # 1402 señala que LLVM realmente no maneja las convenciones de llamadas por completo _ de todos modos_)

Después de leer la función de cambio de pila, la estructura arg no se copia en absoluto entre las pilas, el puntero a la pila anterior simplemente se pasa a la función que se ejecuta en la nueva pila, lo que tiene mucho sentido.

La estructura arg no se copia, pero la función shim cargará valores y los volverá a enviar a la nueva pila. El código antiguo utilizado para escribir literalmente los valores de los argumentos directamente en la pila de destino. Esto tenía sentido en i386 pero en x86_64 es mucho más complejo averiguar qué valores irán a la pila, etc.

Después de algunas pruebas, descubrí que será muy difícil para las funciones de corteza garantizar que no fallan. Lo que sucede actualmente es que, cuando una tarea de nivel superior (como la principal) falla, se le dice a cada tarea que falle, por lo que tan pronto como la devolución de llamada intenta enviar un mensaje (o cuando regresa de enviar un mensaje) puede terminar fallando. y provocando que el tiempo de ejecución se anule de forma anormal.

Supongo que podemos cambiar rust_task para ignorar las solicitudes de eliminación una vez que las tareas hayan vuelto a entrar en la pila de óxido. Para las tareas que implementan bucles de eventos, pueden echar un vistazo a algún puerto de monitor en busca de un mensaje que indique que el tiempo de ejecución está fallando y averiguar cómo terminar correctamente.

Entonces, cuando una tarea de nivel superior falla, ¿propaga los errores a sus hijos? Supongo que no entiendo nuestro modelo de propagación de errores, pensé que iba de las hojas hacia arriba. Parece que el código que se ejecuta en pilas C debería poder ejecutarse en una tarea no supervisada o algo así.

Básicamente actúa como si 'main' estuviera supervisado por el kernel, por lo que si main falla, todo falla.

Estoy llamando a esto hecho. Hay una pequeña limpieza y he presentado errores por separado para los problemas restantes.

Por favor, perdone esta resurrección, pero este problema está vinculado desde una pieza de combinador y y algunos otros sitios, y recientemente un novato me preguntó al respecto, así que estoy notando que, según este problema y cambios posteriores, llamo a Rust desde C es simple :

#[no_mangle]
pub extern fn hello_rust() -> *const u8 {
    "Hello, world!\0".as_ptr()
}
#include "stdio.h"
const char *hello_rust(void);
int main(void) {
    printf("%.32s\n", hello_rust());
}
¿Fue útil esta página
0 / 5 - 0 calificaciones