Dialecto del Contrato Rust
El desarrollo de contratos ocurre en el lenguaje de programación Rust, pero varias características del lenguaje Rust están ya sea no disponibles en el entorno de invitado de despliegue, o no se recomiendan porque su uso incurriría en costos inaceptables en tiempo de ejecución.
Por esta razón, tiene sentido considerar el código escrito para contratos como un dialecto o variante especial del lenguaje de programación Rust, con ciertas restricciones y prioridades inusuales, tales como el determinismo y el tamaño del código.
Estas restricciones y prioridades son similares a las que se encuentran al escribir código Rust para "sistemas embebidos", y las herramientas, bibliotecas y técnicas utilizadas en el "dialecto del contrato" se toman frecuentemente de la comunidad de sistemas embebidos de Rust, y por defecto se recomienda que los contratos se desarrollen con el modo #[no_std]
que excluye completamente la biblioteca estándar de Rust, confiando en la más pequeña biblioteca subyacente core
.
Nota: estas restricciones y prioridades no se aplican al construir en modo de prueba local, y de hecho, las pruebas de contratos locales frecuentemente usarán instalaciones -- para generar entradas de prueba, inspeccionar salidas de prueba y guiar las pruebas -- que no están admitidas en el entorno de invitado de despliegue. Los desarrolladores deben entender la diferencia entre el código que está compilado en módulos de Wasm para despliegue y el código que se compila condicionalmente para pruebas. Consulta depuración de contratos para más detalles.
El "dialecto del contrato" tiene las siguientes características:
No puntos flotantes
La aritmética de punto flotante en el invitado está completamente prohibida. Las operaciones de punto flotante en Wasm tienen algunos aspectos no deterministas o específicos de la plataforma: principalmente patrones de bits NaN, así como configuraciones del entorno de punto flotante como el modo de redondeo.
Si bien es teóricamente posible forzar todo el código de punto flotante a un comportamiento determinista a través de las implementaciones de Wasm, hacerlo en algunas implementaciones de Wasm puede ser difícil, costoso o propenso a errores. Para evitar la complejidad, todo el código de punto flotante es rechazado en el momento de la instanciación.
Esta restricción puede ser revisitada en una versión futura.
Asignación de memoria dinámica limitada (preferiblemente cero)
La asignación de memoria dinámica dentro del invitado es fuertemente desaconsejada, pero no completamente prohibida.
El repertorio de objetos host y funciones host ha sido diseñado para liberar al invitado de tener que realizar asignación dinámica dentro de su propia memoria lineal; en su lugar, se espera que el invitado asigne estructuras dinámicas dentro de objetos host e interactúe con ellas utilizando manejadores livianos.
Usar objetos host en lugar de estructuras de datos en la memoria del invitado tiene numerosos beneficios: rendimiento mucho más alto, tamaño de código mucho menor, interoperabilidad entre contratos, soporte compartido del host para serialización, depuración e introspección de estructuras de datos.
El invitado tiene, sin embargo, una pequeña memoria lineal disponible para él en casos donde la asignación de memoria dinámica es necesaria. Usar esta memoria conlleva costos: el invitado debe incluir en su código una copia completa de un asignador de memoria, y debe pagar el costo de tiempo de ejecución de ejecutar el código del asignador dentro de la VM.
Esta restricción se debe a la limitada capacidad de Wasm para soportar compartición de código: no hay una manera estándar para que el sandbox de Wasm proporcione código compartido de "biblioteca estándar" dentro de un invitado, como un asignador de memoria, ni el host tiene un conocimiento adecuado sobre el contenido de la memoria del invitado para proporcionar un asignador por sí mismo. Cada contrato que desee usar asignación dinámica debe, por lo tanto, llevar su propia copia de un asignador.
Muchas instancias en las que la asignación de memoria dinámica podría parecer ser necesaria también pueden ser atendidas igual de bien con una biblioteca como heapless. Esta biblioteca (y otras de su tipo) proporcionan estructuras de datos con API familiares que aparentan ser dinámicas, pero en realidad están implementadas en términos de una sola pila o asignación estática, con un tamaño máximo fijo establecido en la construcción: los intentos de ampliar el tamaño dinámico más allá del tamaño máximo simplemente fallan. En el contexto de un contrato, este comportamiento puede ser preferible y evita la cuestión de la asignación dinámica por completo.
I/O no estándar
Todas las instalaciones de I/O estándar y el acceso al sistema operativo que un programa Rust típico esperaría realizar utilizando la biblioteca estándar de Rust están prohibidos; los programas que intenten importar tales funciones del host a través de (por ejemplo) la interfaz WASI fallarán en instanciarse, ya que se refieren a funciones no proporcionadas por el host.
No hay sistema operativo, ni ninguna simulación del mismo, presente en el sandbox del contrato. Nuevamente, el repertorio de objetos host y funciones host está destinado a reemplazar y, en gran medida, desvirtuar la necesidad de tales instalaciones de la biblioteca estándar.
Esta restricción surge del hecho de que los contratos necesitan ejecutarse con garantías más fuertes que las ofrecidas por las API típicas de sistemas operativos. Específicamente, los contratos deben realizar I/O con semánticas transaccionales de todo o nada (relativas a su ejecución exitosa o falla) así como consistencia serializable. Esto elimina la mayoría de las API que estarían relacionadas con el I/O típico de archivos. Además, los contratos deben estar aislados de todas las fuentes de no determinismo, como redes o control de procesos, lo que elimina la mayoría de las API restantes. Una vez que se eliminan archivos, redes y control de procesos, simplemente no queda suficiente en las instalaciones estándar de I/O del sistema operativo para molestarse en intentar proporcionarlas.
No hay multihilo
El multihilo no está disponible. Al igual que con las funciones de I/O, intentar importar cualquier API del host relacionada con el multihilo fallará en el momento de la instanciación.
Esta restricción se basa de manera similar en la necesidad de que los contratos se ejecuten en un entorno con fuertes garantías de determinismo y consistencia serializable.
Pánico inmediato
La instalación panic!()
de Rust para errores irrecuperables detendrá inmediatamente la máquina virtual Wasm, deteniendo la ejecución en la instrucción que captura en lugar de deshacer. Esto significa que el código Drop
en tipos de Rust no se ejecutará durante un pánico. Este comportamiento es similar al perfil panic = "abort"
con el que se puede (y a menudo se compila) el código Rust.
Esta no es una restricción dura impuesta por el host, sino una configuración suave realizada a través de una mezcla de funciones de SDK y banderas utilizadas al compilar, con el interés de minimizar el tamaño del código y limitar los costos de ejecución. Se puede eludir con cierto esfuerzo si se desea deshacer y que el código Drop
se ejecute, a costa de un aumento significativo en el tamaño del código.
Colecciones puras-funcionales
Los objetos host tienen una semántica significativamente diferente a las estructuras de datos típicas de Rust, especialmente aquellas que implementan colecciones como mapas y vectores.
En particular: los objetos host son inmutables, y a menudo comparten subestructuras. Por lo tanto, se asemejan más a las estructuras de datos de la programación pura-funcional que al modelo imperativo típico utilizado en muchos programas de Rust.