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.
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.