Tema representativo de entrevista

Entrevista de Rust 2024: ¿Por qué FFI debe usar unsafe extern?

CodingDifícil
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Un crate de Rust no compila tras migrar de la Edición 2021 a la 2024 debido a un bloque extern. Explica la nueva regla, cómo auditar una declaración de ABI de C y cómo encapsular el límite inseguro en una API segura y verificable.

Prompt y contexto aplicable

Un crate de Rust no compila tras migrar de la Edición 2021 a la 2024 debido a que su bloque extern existente ya no es aceptado. Explica por qué se requiere unsafe extern, cómo auditar la declaración de ABI y cómo encapsular el límite inseguro en una API segura y verificable.

Rust 2024 requiere que los bloques foráneos utilicen la palabra clave unsafe. Rust no puede comprobar las firmas, las convenciones de llamada, las variables globales o los contratos de punteros proporcionados por una biblioteca externa, por lo que el autor de la declaración debe asumir la responsabilidad de dichas suposiciones.

Qué evalúa el entrevistador

El entrevistador busca distinguir entre una declaración insegura y una API que expone unsafe en cada punto de llamada. Debes cubrir ABI, anchos de enteros, disposición en memoria (layout), punteros anulables, propiedad, restricciones de hilos, inicialización y un módulo FFI pequeño y auditable.

Preguntas para clarificar

Confirma la Edición del crate, las plataformas de destino, la ABI foránea y la versión de los encabezados. Pregunta si las funciones devuelven recursos con propiedad, qué punteros pueden ser nulos, quién los libera, si los callbacks cruzan hilos y si las versiones de las bibliotecas dinámicas pueden presentar discrepancias. Una solución puramente sintáctica está incompleta sin estas respuestas.

Estructura de respuesta en 30 segundos

“Rust 2024 marca la propia declaración extern como unsafe porque el compilador no puede verificar el contrato de la ABI foránea. Escribiría unsafe extern, auditaría las convenciones de llamada, el layout, los anchos de enteros, la validez de los punteros, las funciones de destrucción y las reglas de hilos, y marcaría una función como safe solo cuando sus precondiciones estén demostradas por el wrapper. Mantendría las condiciones no demostrables como unsafe y luego validaría la migración con compilaciones multiplataforma y pruebas de regresión de ABI”.

Análisis detallado paso a paso

Paso 1: Establecer la responsabilidad de unsafe extern

unsafe extern indica que una declaración puede dar lugar a un comportamiento indefinido y que el autor de la declaración es responsable de su contrato. No valida la implementación en C ni comprueba los argumentos de quienes llaman. La Edición 2024 hace visible esa responsabilidad en el código fuente.

Paso 2: Auditar la ABI y el layout

Verifica las convenciones de llamada como extern "C", el layout de structs, la representación de enums, la alineación, los anchos de enteros y las reglas de valores de retorno. Los encabezados, los bindings generados y la biblioteca vinculada deben describir el mismo contrato versionado; una ejecución local exitosa no constituye evidencia multiplataforma.

Paso 3: Separar las funciones foráneas seguras e inseguras

Las funciones en un bloque foráneo son inseguras por defecto. Una función puede declararse como safe cuando se han demostrado sus precondiciones públicas; en ese caso, los llamadores no necesitan un bloque unsafe, pero el autor de la declaración sigue siendo responsable de dicha prueba. No marques una función desconocida como segura simplemente para reducir la sintaxis de unsafe.

rust
unsafe extern "C" {
    safe fn library_version() -> u32;
    fn library_parse(ptr: *const u8, len: usize) -> i32;
}

Paso 4: Encapsular punteros, propiedad y destrucción

Antes de convertir un puntero crudo en una referencia, valida que no sea nulo, su alineación, longitud y tiempo de vida. Los recursos devueltos por una biblioteca foránea normalmente deben destruirse con su función de liberación correspondiente; el destructor por defecto de Rust u otro asignador no deben utilizarse a través de ese límite.

Paso 5: Verificar las restricciones de hilos y callbacks

Determina si los handles pueden cruzar hilos, si los callbacks se ejecutan en hilos propios de la biblioteca, si los callbacks pueden ser reentrantes y si la destrucción espera a los callbacks. Si esas propiedades no pueden garantizarse estáticamente, restringe el modelo de subprocesos del wrapper y proporciona una barrera de apagado.

Paso 6: Colocar las precondiciones de seguridad en el wrapper

Una función segura debe aceptar tipos de Rust que expresen sus restricciones, como slices, enums o handles con propiedad, en lugar de obligar a cada llamador a pasar un puntero crudo y una longitud. Centraliza las comprobaciones, mantén la operación unsafe en pocas líneas y documenta y prueba cada precondición.

Paso 7: Validar con herramientas de migración y múltiples plataformas de destino

Ejecuta las comprobaciones de migración de la Edición y cargo fix --edition, y luego revisa manualmente los cambios generados en extern. CI debe cubrir plataformas de host y target, compilaciones de depuración y lanzamiento, vinculación estática y dinámica, y regresiones de ABI frente a la versión real de la biblioteca.

Paso 8: Gestionar la discrepancia de versiones y la reversión

Si los encabezados, los bindings y la biblioteca dinámica no coinciden, fija las versiones o vuelve a generar los bindings en lugar de ocultar la discrepancia con conversiones de tipos. Despliega por etapas, preserva una ruta de reversión y monitoriza fallos de carga, cambios en los códigos de error y fugas de recursos.

Ejemplo de respuesta de alta calidad

Separaría la revisión de la declaración de la encapsulación en el punto de llamada. Primero, cambiaría cada bloque foráneo a unsafe extern "C" y compararía la ABI, el layout, la nulabilidad, la propiedad y las funciones de liberación con la versión exacta del encabezado y de la biblioteca. Marcaría solo las funciones con precondiciones públicas demostradas como safe. Luego, colocaría los punteros crudos detrás de un handle de Rust y un módulo FFI basado en slices que compruebe longitudes, inicialización, subprocesos y apagado de callbacks, y siempre liberaría los recursos con la función de la biblioteca. Finalmente, usaría cargo fix --edition para cambios mecánicos, revisaría el diff y ejecutaría pruebas de ABI, rutas de error, apagado concurrente y compatibilidad con bibliotecas dinámicas en múltiples plataformas de destino. Esto mantiene unsafe visible y auditable sin obligar a cada llamador a reproducir el contrato oculto de la biblioteca foránea.

Errores comunes

Añadir unsafe a extern y detenerse ahí

Eso solo soluciona la sintaxis. Firmas, layouts o protocolos de destrucción incorrectos aún pueden causar comportamiento indefinido, por lo que la revisión de contratos y las pruebas de regresión en tiempo de ejecución siguen siendo necesarias.

Marcar cada función foránea como segura

safe es una garantía para los llamadores, no una sugerencia para el compilador. Úsalo solo cuando el wrapper y los tipos impongan continuamente las precondiciones; las funciones desconocidas o con estado global deben permanecer como inseguras.

Liberar un recurso de C con Box de Rust

La destrucción entre diferentes asignadores puede corromper el heap. La biblioteca creadora debe proporcionar la función de liberación, y la implementación de Drop del wrapper debe llamarla con el orden de apagado correcto.

Preguntas de seguimiento y respuestas

¿Qué ocurre si el encabezado de C no documenta el layout del struct?

Trata el layout como un contrato no verificado. Prefiere bindings oficiales o handles opacos. Si un struct debe cruzar el límite, fija las versiones del compilador, la plataforma y la biblioteca, y verifica el tamaño, la alineación y el comportamiento de extremo a extremo en lugar de adivinar los campos.

¿Qué pasa si una actualización de biblioteca cambia el comportamiento tras una declaración segura?

Vuelve a auditar la garantía de seguridad en cada actualización de versión. Fija las versiones de la biblioteca y de los bindings y añade pruebas de compatibilidad para códigos de error, concurrencia y semántica de recursos. Si las mismas precondiciones ya no se cumplen, elimina safe y gestiona la condición explícitamente en el wrapper.

¿Cómo debe una API de Rust gestionar callbacks que pueden ejecutarse en cualquier hilo?

No pases una clausura arbitraria de Rust con estado mutable compartido directamente a C. Utiliza un canal seguro para hilos o un ejecutor controlado, define el tiempo de vida del callback y una barrera de apagado, y expón una interfaz segura solo cuando se cumplan Send, la sincronización y los requisitos de tiempo de vida.

Fuentes públicas

Preguntas relacionadas

Herramienta de entrevista relacionada

Usa Captura para un ejercicio de código

Captura el problema y luego aborda en orden las restricciones, la solución, el código, los casos extremos y la complejidad.

Ver la herramienta