Capítulo 4. Memoria de WebAssembly

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Quizá algún día también esto sea agradable de recordar.

Virgil

Si WebAssembly va a comportarse como un entorno de ejecución normal, necesita una forma de asignar y liberar memoria para sus actividades de manejo de datos. En este capítulo, te presentaremos cómo emula este comportamiento para que sea eficiente, pero sin el riesgo de los típicos problemas de manipulación de memoria que se ven con lenguajes como C y C++ (aunque sea eso lo que estamos ejecutando). Como potencialmente estamos descargando código arbitrario a través de Internet, ésta es una importante consideración de seguridad.

Todo el concepto de computación suele implicar alguna forma de procesamiento de datos. Tanto si estamos corrigiendo la ortografía de un documento, manipulando una imagen, haciendo aprendizaje automático, secuenciando proteínas, jugando a videojuegos, viendo películas o simplemente haciendo números en una hoja de cálculo, generalmente estamos interactuando con bloques arbitrarios de datos. Una de las consideraciones de rendimiento más cruciales en estos sistemas es cómo llevar los datos a donde deben estar para interrogarlos o transformarlos de algún modo.

Las Unidades Centrales de Proceso (CPU) funcionan más rápido cuando los datos están disponibles en un registro o en una caché en chip.1 Obviamente, se trata de contenedores muy pequeños, por lo que los grandes conjuntos de datos nunca se van a cargar en la CPU en su totalidad. Tenemos que dedicar cierto esfuerzo a mover los datos dentro y fuera de la memoria. El coste de esperar a que los datos se carguen en una de estas ubicaciones es una eternidad en tiempo de reloj de la CPU. Ésta es una de las razones por las que se han vuelto tan complejos. Los chips modernos disponen de todo tipo de multipipeline, bifurcación predictiva y reescritura de instrucciones para mantener ocupado al chip mientras leemos de una red a la memoria principal, de ahí a las cachés multinivel y, finalmente, a donde hay que utilizarla.

Los programas tradicionales han tenido normalmente memoria de pila para gestionar variables a corto plazo de tamaños pequeños o fijos. Utilizan memoria basada en el montón para bloques de datos a más largo plazo y de tamaño arbitrario. En general, no son más que diferentes áreas de la memoria asignada a un programa que se tratan de forma diferente. La memoria de pila se sobrescribe con frecuencia por el flujo y reflujo de las funciones que se llaman durante la ejecución. La memoria de montón se utiliza y se limpia cuando ya no se necesita. Si un programa se queda sin memoria, puede pedir más, pero debe ser razonablemente juicioso sobre cómo la utiliza.2 Hoy en día, los sistemas de paginación virtual y el abaratamiento de la memoria hacen totalmente probable que un ordenador típico pueda tener decenas de gigabytes de memoria. Ser capaz de acceder rápida y eficazmente a bytes individuales de conjuntos de datos potencialmente grandes es una clave importante para un rendimiento decente del software en tiempo de ejecución.

Los programas WebAssembly necesitan una forma de simular estos bloques de memoria sin dar realmente acceso sin restricciones a la privacidad de la memoria de nuestro ordenador. Afortunadamente, aquí hay una buena historia que equilibra comodidad, velocidad y seguridad. Comienza haciendo posible que JavaScript acceda a bytes individuales de la memoria, pero se expandirá más allá de JavaScript para ser una forma genérica de compartir memoria entre entornos anfitriones y módulos WebAssembly.

TypedArrays

Tradicionalmente, JavaScript no ha sido capaz de proporcionar un acceso cómodo a bytes individuales de la memoria. Por eso, la funcionalidad sensible al tiempo y de bajo nivel suele proporcionarla el navegador o algún tipo de complemento. Incluso las aplicaciones Node.js a menudo tienen que implementar algunas funcionalidades en un lenguaje que maneja la manipulación de la memoria mejor de lo que puede hacerlo JavaScript. Esto complica la situación, ya que JavaScript es un lenguaje interpretado y necesitarías un mecanismo eficiente para cambiar el flujo de control de un lado a otro entre el código interpretado, portable, y el código compilado rápido. Esto también complica las Implementaciones, porque una parte de la aplicación es intrínsecamente portátil y otra necesita soporte de bibliotecas nativas en distintos sistemas operativos.

Suele haber un compromiso en el desarrollo de software: los lenguajes o son rápidos o son seguros. Cuando necesitas velocidad bruta, puedes elegir C o C++, ya que proporcionan muy pocas comprobaciones en tiempo de ejecución en el uso y manipulación de datos en memoria. En consecuencia, son muy rápidos. Cuando quieras seguridad, puedes elegir un lenguaje con comprobaciones de límites en tiempo de ejecución en las referencias a matrices. El inconveniente de la compensación por la velocidad es que las cosas son lentas o la carga de la gestión de la memoria recae en elprogramador. Desgraciadamente, es muy fácil meter la pata olvidando asignar espacio, reutilizando la memoria liberada o no liberando el espacio cuando has terminado. Ésta es una de las razones por las que las aplicaciones escritas en estos lenguajes rápidos suelen tener fallos, se bloquean con facilidad y son fuente de muchas vulnerabilidades de seguridad.3

Los lenguajes con recolección de basura, como Java y JavaScript, liberan a los desarrolladores de muchas de las cargas de gestionar la memoria, pero a menudo incurren en una carga de rendimiento en tiempo de ejecución como contrapartida. Una parte del tiempo de ejecución debe buscar constantemente la memoria no utilizada y liberarla. La sobrecarga de rendimiento hace que muchas de estas aplicaciones sean impredecibles y, por tanto, inadecuadas para aplicaciones integradas, sistemas financieros u otros casos de uso sensibles al tiempo.

Asignar memoria no es un gran problema, siempre que la que se cree tenga un tamaño adecuado para lo que quieras poner en ella. Lo complicado es saber cuándo liberar. Obviamente, liberar memoria antes de que un programa haya terminado de utilizarla es malo, pero no hacerlo cuando ya no se necesita es ineficaz y podrías quedarte sin memoria. Lenguajes como Rust consiguen un buen equilibrio entre comodidad y seguridad. El compilador te obliga a comunicar tus intenciones más claramente, pero cuando lo haces, puede ser más eficaz a la hora de limpiar después de ti.

Cómo se gestiona todo esto en tiempo de ejecución suele ser una de las características que definen un lenguaje y su tiempo de ejecución. Como tal, no todos los lenguajes requieren el mismo nivel de soporte. Ésta es una de las razones por las que los diseñadores de WebAssembly no sobreespecificaron características como la recogida de basura en el MVP.

JavaScript es un lenguaje dinámico flexible y , pero históricamente no ha facilitado ni hecho eficiente el tratamiento de bytes individuales de grandes conjuntos de datos. Esto complica el uso de bibliotecas de bajo nivel, ya que los datos tienen que copiarse dentro y fuera de formatos nativos de JavaScript, lo que resulta ineficaz. La clase Array almacena objetos JavaScript, lo que significa que tiene que estar preparada para tratar con tipos arbitrarios. Muchos de los contenedores flexibles de Python también son igualmente flexibles e hinchados.4 El rápido recorrido y manipulación de la memoria mediante punteros es producto de la uniformidad de los tipos de datos en bloques contiguos. Los bytes son la unidad mínima direccionable, sobre todo cuando se trata de imágenes, vídeos y archivos de sonido.

Los datos numéricos requieren más esfuerzo. Un entero de 16 bits ocupa dos bytes. Un entero de 32 bits, cuatro. La posición 0 de una matriz de bytes puede representar el primer número de este tipo en una matriz de datos, pero el segundo empezará en la posición 4.

JavaScript añadió las interfaces TypedArray para abordar estos problemas, inicialmente en el contexto de la mejora del rendimiento de WebGL. Se trata de porciones de memoria disponibles a través de instancias ArrayBuffer que pueden tratarse como bloques homogéneos de tipos de datos concretos. La memoria disponible se limita a la instancia ArrayBuffer, pero puede almacenarse internamente en un formato que resulte conveniente pasar a las bibliotecas nativas.

En el Ejemplo 4-1, vemos la funcionalidad básica de crear una matriz tipada de enteros sin signo de 32 bits.

Ejemplo 4-1. Diez enteros de 32 bits creados en un Uint32Array
var u32arr = new Uint32Array(10);
u32arr[0] = 257;
console.log(u32arr);
console.log("u32arr length: " + u32arr.length);

La salida de la invocación debe tener este aspecto:

Uint32Array(10) [ 257, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
u32arr length: 10

Como puedes ver, funciona como cabría esperar de una matriz de enteros. Ten en cuenta que se trata de enteros de 4 bytes (de ahí el 32 en el nombre del tipo). En el Ejemplo 4-2, recuperamos elArrayBuffer subyacente del Uint32Array y lo imprimimos. Esto nos muestra que su longitud es 40. A continuación, envolvemos el búfer con un Uint8Arrayque representa una matriz de bytes sin signo e imprimimos su contenido y longitud.

Ejemplo 4-2. Acceder a los enteros de 32 bits como un buffer de bytes de 8 bits
var u32buf = u32arr.buffer;
var u8arr = new Uint8Array(u32buf);
console.log(u8arr);
console.log("u8arr length: " + u8arr.length);

El código produce el siguiente resultado:

ArrayBuffer { byteLength: 40 }
Uint8Array(40) [ 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, … ]
u8arr length: 40

El ArrayBuffer representa los bytes subyacentes en bruto. El TypedArrayes una vista interpretada de esos bytes basada en el tamaño de tipo especificado. Así, cuando inicializamos el Uint32Array con una longitud de 10, eso significaba diez enteros de 32 bits, que requieren 40 bytes para representarse. El búfer separado se configura para que sea así de grande, de modo que pueda contener los 10 enteros. El Uint8Array trata cada byte como un elemento individual debido a sudefinición de tamaño.

Si echas un vistazo a la Figura 4-1, es de esperar que veas lo que está pasando. El primer elemento (posición 0) del Uint32Array es simplemente el valor 257. Ésta es una vista interpretada de los bytessubyacentes en el ArrayBuffer. El Uint8Array refleja directamente los bytessubyacentes del buffer. Los patrones de bits de la parte inferior del diagrama reflejan los bits por byte de los dos primeros bytes.

wadg 0401
Figura 4-1. Representación del valor 257

Puede sorprenderte que en haya 1s en los dos primeros bytes. Esto se debe a una confusa noción llamada endianidad que aparece cuando almacenamos números en la memoria.5 En este caso, un sistema little endian almacena primero los bytes menos significativos (los 1s). Un sistemabig endian almacenaría primero los 0. En el gran esquema de las cosas, no importa cómo se almacenen, pero diferentes sistemas y protocolos elegirán uno u otro. Sólo tienes que fijarte en qué formato estás viendo.

Como ya se ha indicado, las clases TypedArray se introdujeron inicialmente para WebGL, pero desde entonces han sido adoptadas por otras API, como Canvas2D, XMLHttpRequest2, File, Binary WebSockets, etc. Fíjate en que todas ellas son API de E/S y visualización de bajo nivel y orientadas al rendimiento que tienen que interactuar con bibliotecas nativas. La representación de memoria subyacente puede pasarse entre estas capas de forma eficiente. Por estas razones, también son útiles para las instancias de WebAssembly Memory.

Instancias de memoria de WebAssembly

Un WebAssembly Memory es un ArrayBuffer subyacente (oSharedArrayBuffer, como veremos más adelante) asociado a un módulo. Por el momento, el MVP limita un módulo a tener una sola instancia, pero es probable que esto cambie dentro de poco. Un módulo puede crear su propia instancia Memory, o puede recibir una de su entorno anfitrión. Estas instancias pueden importarse o exportarse como hemos hecho hasta ahora con las funciones. También hay una sección Memoryasociada en la estructura del módulo que nos saltamos en el Capítulo 3 porque aún no habíamos tratado el concepto. Ahora subsanaremos esa omisión.

En el Ejemplo 4-3, tenemos un archivo Wat que define una instancia Memoryy la exporta con el nombre "memory". Esto representa un bloque contiguo de memoria limitado a una instancia ArrayBufferconcreta. Es el principio de nuestra capacidad para emular matrices homogéneas de bytes en memoria similares a las de C/C++. Cada instancia está formada por uno o varios bloques de 64 kilobytes de páginas de memoria. En el ejemplo, lo inicializamos con una sola página, pero permitimos que crezca hasta 10 páginas para un total de 640 kilobytes, que debería ser suficiente para cualquiera.6 Verás cómo aumentar la memoria disponible momentáneamente. Por ahora, sólo vamos a escribir los bytes 1, 1, 0 y 0 al principio de la memoria intermedia. La instrucción i32.const carga un valor constante en la pila. Queremos escribir al principio de nuestro búfer, así que utilizamos el valor 0x0. La instrucción data es útil para inicializar partes de nuestra instancia Memory.

Ejemplo 4-3. Creación y exportación de una instancia de Memory en un módulo WebAssembly
(module
  (memory (export "memory") 1 10)
  (data (i32.const 0x0) "\01\01\00\00")
)

Si compilamos este archivo a su representación binaria con wat2wasmy luego invocamos wasm-objdump, veremos algunos detalles nuevos que aún no habíamos encontrado:

brian@tweezer ~/g/w/s/ch04> wasm-objdump -x memory.wasm

memory.wasm:	file format wasm 0x1

Section Details:

Memory[1]:
 - memory[0] pages: initial=1 max=10
Export[1]:
 - memory[0] -> "memory"
Data[1]:
 - segment[0] memory=0 size=4 - init i32=0
  - 0000000: 0101 0000

Hay una instancia Memory configurada en la sección Memory que refleja nuestro tamaño inicial de una página y un tamaño máximo de 10 páginas. Vemos que se exporta como "memory" en la sección Export. También vemos que la sección Data ha inicializado nuestra instancia de memoria con los cuatro bytes que escribimos en ella.

Ahora podemos utilizar nuestra memoria exportada importándola en algún JavaScript del navegador. En este ejemplo, cargaremos el módulo y obtendremos la instancia Memory. A continuación, mostraremos el tamaño del búfer en bytes, el número de páginas y lo que hay actualmente en el búfer de memoria.

La estructura básica de nuestro archivo HTML se muestra enel Ejemplo 4-4. Tenemos una serie de elementos <span> que se rellenarán con los detalles mediante una función llamada show​De⁠tails(), que tomará una referencia a nuestra instancia de memoria.

Ejemplo 4-4. Mostrar detalles de Memory en el navegador
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="bootstrap.min.css">
    <title>Memory</title>
    <script src="utils.js"></script>
  </head>
  <body>
    <div class="container">
      <h1>Memory</h1>
      <div>Your memory instance is <span id="mem"></span> bytes.</div>
      <div>It has this many pages: <span id="pages"></span>.</div>
      <div>Uint32Buffer[0] = <span id="firstint"></span>.</div>
      <div>Uint8Buffer[0-4] = <span id="firstbytes"></span>.</div>
    </div>

    <button id="expand">Expand</button>

    <script>
       <!-- Shown below -->
    </script>
  </body>
</html>

En el Ejemplo 4-5, vemos el JavaScript para nuestro elemento <script>. Fíjate primero en la llamada a fetchAndInstantiate(). Se comporta de la misma forma que hemos visto antes en cuanto a la carga del módulo. Aquí obtenemos una referencia a la instancia Memory a través de la secciónexports. Adjuntamos una función onClick() para nuestro botón que abordaremos momentáneamente.

Ejemplo 4-5. El código JavaScript de nuestro ejemplo
function showDetails(mem) {
  var buf = mem.buffer;
  var memEl = document.getElementById('mem');
  var pagesEl = document.getElementById('pages');
  var firstIntEl = document.getElementById('firstint');
  var firstBytesEl = document.getElementById('firstbytes');

  memEl.innerText=buf.byteLength;
  pagesEl.innerText=buf.byteLength / 65536;

  var i32 = new Uint32Array(buf);
  var u8 = new Uint8Array(buf);

  firstIntEl.innerText=i32[0];
  firstBytesEl.innerText= "[" + u8[0] + "," + u8[1] + "," +
                                u8[2] + "," + u8[3] + "]";
};

fetchAndInstantiate('memory.wasm').then(function(instance) {
  var mem = instance.exports.memory;

  var button = document.getElementById("expand");
  button.onclick = function() {
    try {
       mem.grow(1);
       showDetails(mem);
    } catch(re) {
       alert("You cannot grow the Memory any more!");
    };
  };
  showDetails(mem);
});

Por último, llamamos a la función showDetails() y pasamos nuestra variable mem. Esta función recuperará el ArrayBuffer subyacente y las referencias a nuestros distintos elementos <span> para mostrar los detalles. La longitud del búfer se almacena en el campo innerText de nuestro primer<span>. El número de páginas es esta longitud dividida por 64 KB para indicar el número de páginas. A continuación, envolvemos el ArrayBuffer con unUint32Array, que nos permite obtener nuestros valores de memoria como enteros de 4 bytes. El primer elemento de esto se muestra en el siguiente <span>. También envolvemos nuestro ArrayBuffer en Uint8Array y mostramos los cuatro primeros bytes. Después de nuestra discusión anterior, los detalles mostrados enla Figura 4-2 no deberían sorprenderte.

wadg 0402
Figura 4-2. Muestra los detalles de nuestra Memory

La función onClick() llama a un método de la instancia Memory para aumentar el tamaño asignado en una página de memoria. Esto hace que el ArrayBuffer original se desprenda de la instancia, y los datos existentes se copian encima. Si tenemos éxito, volvemos a invocar la funciónshowDetails() y extraemos el nuevo ArrayBuffer. Si se pulsa el botón una vez, deberías ver que la instancia representa ahora dos páginas de memoria que representan 128 KB de memoria. Los datos del principio no deberían haber cambiado.

Si pulsas el botón demasiadas veces, el número de páginas asignadas superará la cantidad máxima especificada en de 10 páginas. En ese momento, ya no será posible ampliar la memoria y se lanzará un mensaje RangeError. Nuestro ejemplo mostrará una ventana de alerta cuando esto ocurra.

Uso de la API de memoria de WebAssembly

El método grow() que hemos utilizado en el ejemplo anterior forma parte de la API JavaScript de WebAssembly que el MVP espera que proporcionen todos los entornos de host. Podemos ampliar nuestro uso de esta API e ir en la otra dirección. es decir, podemos crear unaMemory instancia en JavaScript y ponerla a disposición de un módulo. Ten en cuenta el límite actual de una instancia por módulo.

En capítulos posteriores, veremos usos más elaborados de la memoria, pero tendremos que utilizar un lenguaje de nivel superior al Wat para hacer algo serio. Por ahora, mantendremos nuestro ejemplo en el lado más sencillo, pero intentaremos ampliarlo más allá de lo que hemos visto.

Empezaremos con el HTML para que puedas ver todo el flujo de trabajo, y luego nos sumergiremos en los detalles del nuevo módulo. Enel Ejemplo 4-6, puedes ver que estamos utilizando una estructura HTML similar a la que hemos utilizado hasta ahora. Hay un elemento <div> con el ID de container en el que colocaremos una serie de números de Fibonacci. Si no estás familiarizado con estos números, son muy importantes en muchos sistemas naturales, y te animamos a que los investigues por tu cuenta. Los dos primeros números se definen como 0 y 1. Los números siguientes se definen como la suma de los dos anteriores. Así, el tercer número será 1 (0 + 1). El cuarto número será "2" (1 + 1). El quinto número será 3 (2 + 1), etc.

Ejemplo 4-6. Crear un Memory en JavaScript e importarlo al módulo
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="bootstrap.min.css">
    <title>Fibonacci</title>
    <script src="utils.js"></script>
  </head>
  <body>
    <div id="container"></div>

    <script>
      var memory = new WebAssembly.Memory({initial:10, maximum:100});

      var importObject = {
        js: { mem: memory }
      };

      fetchAndInstantiate('memory2.wasm', importObject).then(function(instance) {
	  var fibNum = 20;
	  instance.exports.fibonacci(fibNum);
	  var i32 = new Uint32Array(memory.buffer);

	  var container = document.getElementById('container');

	  for(var i = 0; i < fibNum; i++) {
	      container.innerText += `Fib[${i}]: ${i32[i]}\n`;
	  }
      });

    </script>
  </body>
</html>

El cálculo real está escrito en Wat y se muestra enel Ejemplo 4-7, pero antes de llegar a él, vemos la creación de la instancia Memory en la primera línea del elemento <script>. Estamos utilizando la API de JavaScript, pero la intención es la misma que la de nuestro uso del elemento (memory) en el Ejemplo 4-3. Creamos un tamaño inicial de una página de memoria y un tamaño máximo de 10 páginas. En este caso, nunca necesitaremos más de una página, pero ahora verás cómo hacerlo. La instancia Memory se pone a disposición del módulo a través deimportObject. Como verás enseguida, la función del módulo Wasm tomará un parámetro que indica cuántos números de Fibonacci escribir en el búfer Memory. En este ejemplo, pasaremos un parámetro de 20.

Una vez instanciado nuestro módulo, llamamos a su función exportada fibonacci(). Tenemos acceso a la variable memory desde arriba, así que podemos recuperar el ArrayBuffer subyacente directamente una vez finalizada la invocación a la función. Como los números de Fibonacci son enteros, envolvemos el búfer en una instancia de Uint32Array para poder iterar sobre los elementos individuales. Al recuperar los números, no tenemos que preocuparnos de que sean enteros de 4 bytes. Al leer cada valor, ampliamos el innerText de nuestro elemento container con una versión en cadena del número.

El cálculo que se muestra en el Ejemplo 4-7 va a ser bastante más complicado que cualquier Wat que hayamos visto hasta ahora, pero abordándolo por partes deberías ser capaz de resolverlo.

Ejemplo 4-7. Cálculos de Fibonacci expresados en Wat
(module
  (memory (import "js" "mem") 1) 1
  (func (export "fibonacci") (param $n i32) 2
    (local $index i32) 3
    (local $ptr i32) 4

    (i32.store (i32.const 0) (i32.const 0)) 5
    (i32.store (i32.const 4) (i32.const 1))

    (set_local $index (i32.const 2)) 6
    (set_local $ptr (i32.const 8))

    (block $break 7
      (loop $top 8
        (br_if $break (i32.eq (get_local $n) (get_local $index))) 9
        (i32.store 10
          (get_local $ptr)
          (i32.add
            (i32.load (i32.sub (get_local $ptr) (i32.const 4)))
  	    (i32.load (i32.sub (get_local $ptr) (i32.const 8)))
	  )
        )
	(set_local $ptr (i32.add (get_local $ptr) (i32.const 4))) 11
	(set_local $index (i32.add (get_local $index) (i32.const 1)))
	(br $top) 12
      )
    )
  )
)
1

El Memory se importa desde el entorno anfitrión.

2

La función fibonacci está definida y exportada.

3

$index es nuestro contador de números.

4

$ptr es nuestra posición actual en la instancia Memory.

5

La función i32.store escribe un valor en la ubicación especificada del búfer.

6

La variable $index avanza a 2 y la $ptr se ajusta a 8.

7

Definimos un bloque con nombre al que volver en nuestros bucles.

8

Definimos un bucle con nombre en nuestro bloque.

9

Salimos de nuestro bucle cuando la variable $index es igual al parámetro $n.

10

Escribimos la suma de los dos elementos anteriores en la ubicación actual de $ptr.

11

Avanzamos la variable $ptr en 4 y la variable $index en 1.

12

Rompemos al principio de nuestro bucle.

Esperemos que las notas numéricas adjuntas al Ejemplo 4-7 tengan sentido, pero dada su complejidad, merece una rápida discusión. Se trata de una máquina virtual basada en la pila, por lo que todas las instrucciones implican manipular la parte superior de la pila. En la primera llamada, importamos la memoria definida en el JavaScript. Representa la asignación por defecto de una página, que debería ser suficiente por ahora. Aunque ésta es una implementación correcta, no es una implementación demasiado segura. Las entradas erróneas podrían estropear el flujo, pero eso nos preocupará más cuando introduzcamos la compatibilidad con lenguajes de alto nivel, donde es más fácil manejar esos detalles.

La función exportada está definida para tomar un parámetro $n que representa el número de números de Fibonacci que hay que calcular.7 Utilizamos dos variables locales definidas en la tercera y cuarta llamadas. La primera representa el número con el que estamos trabajando y por defecto es 0. La segunda actuará como puntero en memoria. Servirá como índice dentro delMemory memoria intermedia. Recuerda que los valores de datos de i32 representan 4 bytes, por lo que cada avance de$index implicará avanzar $ptr en 4. No tenemos la ventaja de TypedArrays en este lado de la interacción, así que tenemos que manejar estos detalles nosotros mismos. De nuevo, los lenguajes de alto nivel nos protegerán de muchos de estos detalles.

Por definición, los dos primeros números de Fibonacci son 0 y 1, así que los escribimos en el búfer. i32.store escribe un valor entero en una ubicación. Espera encontrar esos valores en la parte superior de la pila, así que las dos partes siguientes de la sentencia invocan la instrucción i32.const, que empuja los valores especificados a la parte superior de la pila. En primer lugar, un desplazamiento de 0 indica que queremos escribir al principio de la memoria intermedia. La segunda empuja el número 0 a la pila para indicar el valor que queremos escribir en la posición 0. La línea siguiente repite el proceso para el siguiente número de Fibonacci. El i32 de la línea anterior ocupa 4 bytes, así que escribimos el valor 1 en la posición 4.

El siguiente paso es iterar sobre los números restantes, que se definen cada uno como la suma de los dos anteriores. Por eso necesitamos iniciar el proceso con los dos que acabamos de escribir. Avanzamos nuestra variable $indexhasta 2, por lo que necesitaremos $n - 2 iteraciones del bucle. Hemos escrito dos enteros i32, así que avanzamos nuestra $ptr a 8.

Wat hace referencia a varias instrucciones de WebAssembly que conocerás a lo largo del libro. Aquí puedes ver algunas de las construcciones de bucle. Definimos un bloque en la séptima llamada y le damos la etiqueta $break. El siguiente paso introduce un bucle con un punto de entrada llamado $top. La primera instrucción del bucle comprueba si $n y $index son iguales, lo que indica que hemos manejado todos nuestros números. Si es así, sale del bucle. Si no, continúa.

La instrucción i32.store de la 10ª llamada escribe en la ubicación $ptr. Los valores de las variables se empujan a la parte superior de la pila con get_local. El valor que se escribe allí es la suma de los valores de los dos números anteriores. i32.add espera encontrar también sus dos sumandos en la parte superior de la pila. Así que cargamos la posición del número entero que es cuatro menos que $ptr. Esto representa $n - 1. A continuación, cargamos el entero almacenado en la ubicación de $ptr menos 8, que representa $n - 2. i32.add saca estos sumandos de la parte superior de la pila y escribe su suma de nuevo en la parte superior. La pila contiene ahora este valor en la parte superior y la ubicación del valor actual $ptr, que es lo que espera i32.store.

El siguiente paso avanza $ptr en cuatro, ya que ahora hemos escrito otro número de Fibonacci en la memoria intermedia. Avanzamos $n en uno y, a continuación, volvemos al principio del bucle y repetimos el proceso. Una vez que hemos escrito $n números en la memoria, la función vuelve. No es necesario que devuelva nada, ya que el entorno anfitrión tiene acceso al búferMemory y puede leer los resultados directamente conTypedArrays, como hemos visto antes.

El resultado de cargar nuestro HTML en el navegador y mostrar los 20 primeros números de Fibonacci se muestra en la Figura 4-3.

wadg 0403
Figura 4-3. Lectura de la secuencia de Fibonacci desde la instancia Memory

Este nivel de detalle sería molesto de tratar regularmente, pero afortunadamente no tendrás que hacerlo. Sin embargo, es importante entender cómo funcionan las cosas a este nivel, y cómo podemos emular bloques continuos de memoria lineal para un procesamiento eficiente.

¡Por fin cuerdas!

Una última discusión antes de seguir adelante es sobre cómo podemos añadir por fin cadenas a nuestro repertorio. En encontrarás muchas más herramientas que te facilitarán aún más las cosas en capítulos posteriores del libro, pero podemos aprovechar algunas ventajas de Wat para escribir cadenas en el búfer Memoryy leerlas en JavaScript.

En el Ejemplo 4-8, puedes ver un módulo muy sencillo que exporta una instancia de Memory de una página. A continuación, utiliza una instrucción data para escribir una secuencia de bytes en una ubicación de la memoria del módulo. Comienza en la ubicación 0 y escribe los bytes en la cadena subsiguiente. Es conveniente no tener que convertir las cadenas multibyte en los bytes que las componen, aunque puedes hacerlo si quieres. Esta cadena tiene una frase en japonés y luego su traducción al inglés.8

Ejemplo 4-8. Un uso sencillo de las cadenas en Wat
(module
 (memory (export "memory") 1)
 (data (i32.const 0x0) "私は横浜に住んでいました。I used to live in Yokohama.")
)

Una vez que compilemos el Wat a Wasm, veremos que tenemos una nueva sección poblada en nuestro módulo. Puedes verlo con el comando wasm-objdump:

brian@tweezer ~/g/w/s/ch04> wasm-objdump -x strings.wasm

strings.wasm:	file format wasm 0x1

Section Details:

Memory[1]:
 - memory[0] pages: initial=1
Export[1]:
 - memory[0] -> "memory"
Data[1]:
 - segment[0] memory=0 size=66 - init i32=0
  - 0000000: e7a7 81e3 81af e6a8 aae6 b59c e381 abe4  ................
  - 0000010: bd8f e382 93e3 81a7 e381 84e3 81be e381  ................
  - 0000020: 97e3 819f e380 8249 2075 7365 6420 746f  .......I used to
  - 0000030: 206c 6976 6520 696e 2059 6f6b 6f68 616d   live in Yokoham
  - 0000040: 612e

Las secciones Memory, Export, y Data se rellenan con los detalles de nuestras cadenas escritas en memoria. La instancia se inicializa de esta forma para que cuando un entorno anfitrión lea del búfer, las cadenas ya estén allí.

En el Ejemplo 4-9, ves que tenemos un <span> para nuestra frase en japonés y otro para nuestra frase en inglés. Para extraer los bytes individuales, podemos envolver un Uint8Array alrededor del búfer de instancia Memoryque hemos importado del módulo. Observa que sólo envolvemos los 39 primeros bytes. Estos bytes se descodifican a una cadena UTF-8 mediante una instancia TextDecoder, y luego fijamos el innerTextdel <span> designado para la frase japonesa. A continuación, envolvemos con otro Uint8Array la parte del búfer que comienza en la posición 39 e incluye los 26 bytes siguientes.

Ejemplo 4-9. Lectura de cadenas de una instancia importada de Memory
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="bootstrap.min.css">
    <title>Reading Strings From Memory</title>
    <script src="utils.js"></script>
  </head>
  <body>
    <div>
      <div>Japanese: <span id="japanese"></span></div>
      <div>English: <span id="english"></span></div>
    </div>
    <script>
      fetchAndInstantiate('strings.wasm').then(function(instance) {
	  var mem = instance.exports.memory;

	  var bytes = new Uint8Array(mem.buffer, 0, 39);
          var string = new TextDecoder('utf8').decode(bytes);
          var japanese = document.getElementById('japanese');
	  japanese.innerText = string;

	  bytes = new Uint8Array(mem.buffer, 39, 26);
	  string = new TextDecoder('utf8').decode(bytes);
          var english = document.getElementById('english');
	  english.innerText = string;
      });

    </script>
  </body>
</html>

En la Figura 4-4, vemos los resultados satisfactorios de leer los bytes del búfer y renderizarlos como cadenas UTF-8.

wadg 0404
Figura 4-4. Lectura de cadenas desde la instancia Memory

Por geniales que sean estos resultados, ¿cómo supimos cuántos bytes envolver y en qué lugar buscar las cadenas? Un poco de trabajo detectivesco puede ayudar. La letra "I" mayúscula se representa como 49 en hexadecimal. La salida de wasm-objdump nos da el desplazamiento en el segmento Data para cada byte. Vemos el valor 49 por primera vez en la fila que comienza con 0000020:. El 49 representa el séptimo byte, por lo que la segunda frase comienza en la posición 27, que es 2 × 16 + 7 en decimal, es decir, 39. La cadena japonesa representa los bytes entre 0 y 39. La cadena inglesa comienza en la posición 39.

Pero, ¡un momento! Resulta que hemos contado mal la frase en inglés y nos hemos equivocado por una. Esto parece un esfuerzo molesto y propenso a errores para sacar cadenas de un módulo WebAssembly. Incluso haciendo las cosas por las malas a este bajo nivel se puede hacer mejor. Escribiremos primero las ubicaciones de las cadenas para no tener que averiguarlo por nuestra cuenta.

Mira el Ejemplo 4-10 para ver cómo podemos ser más sofisticados. Ahora tenemos dos segmentos data. El primero escribe la posición inicial y la longitud de la primera cadena, seguida de la misma información para la segunda. Como estamos utilizando el mismo búfer para los índices y las cadenas, tenemos que tener cuidado con las ubicaciones.

Como nuestras cadenas no son muy largas, podemos utilizar bytes sueltos como desplazamientos y longitudes. Probablemente no sea una buena estrategia en general, pero mostrará cierta flexibilidad adicional. Así, escribimos el valor 4 y el valor 27. Esto representa un desplazamiento de 4 bytes y una longitud de 39. El desplazamiento es de 4 porque tenemos estos cuatro números (como bytes individuales) al principio del búfer y tendremos que saltarlos para llegar a las cadenas. Como ya sabes, 27 es el hexadecimal de 39, la longitud de la cadena japonesa. La frase en inglés comenzará en el índice 4 + 39 = 43, que es 2b en hexadecimal (2 × 16 + 11) y tiene 27 bytes de longitud, que es 1b en hexadecimal (1 × 16 + 11).

El segundo segmento de data empieza en la posición 0x4 porque necesitamos saltarnos esos desplazamientos y longitudes.

Ejemplo 4-10. Un uso más sofisticado de las cadenas en Wat
(module
 (memory (export "memory") 1)
 (data (i32.const 0x0) "\04\27\2b\1b")
 (data (i32.const 0x4) "私は横浜に住んでいました。I used to live in Yokohama.")
)

En el Ejemplo 4-11, vemos la otra cara de la lectura de las cadenas. Ciertamente, ahora es más complicado, pero también es menos manual, ya que el módulo nos dice exactamente dónde buscar. Otra opción al utilizar TypedArrays es DataView, que te permite extraer tipos de datos arbitrarios del búfer Memory. No es necesario que sean homogéneos como el TypedArrays normal (por ejemplo, Uint32Array).

Ejemplo 4-11. Lectura de nuestras cadenas indexadas desde el búfer Memory
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="bootstrap.min.css">
    <title>Reading Strings From Memory</title>
    <script src="utils.js"></script>
  </head>
  <body>
    <div>
      <div>Japanese: <span id="japanese"></span></div>
      <div>English: <span id="english"></span></div>
    </div>
    <script>
      fetchAndInstantiate('strings2.wasm').then(function(instance) {
	  var mem = instance.exports.memory;

	  var dv = new DataView(mem.buffer);
	  var start = dv.getUint8(0);
	  var end = dv.getUint8(1);

	  var bytes = new Uint8Array(mem.buffer, start, end);
	  var string = new TextDecoder('utf8').decode(bytes);
          var japanese = document.getElementById('japanese');
	  japanese.innerText = string;

	  start = dv.getUint8(2);
	  end = dv.getUint8(3);

	  bytes = new Uint8Array(mem.buffer, start, end);
	  string = new TextDecoder('utf8').decode(bytes);
          var english = document.getElementById('english');
	  english.innerText = string;
      });

    </script>
  </body>
</html>

Por tanto, envolvemos el búfer exportado Memory con una instancia DataViewy leemos los dos primeros bytes llamando a la función getUint8()una vez en la posición 0 y otra en la posición 1. Éstos representan la posición y el desplazamiento en el búfer de la cadena japonesa. Aparte de dejar de utilizar números codificados, el resto de nuestro código anterior es el mismo. A continuación leemos los dos bytes de las posiciones 2 y 3, que representan la posición y la longitud de la frase en inglés. Esto también se convierte en una cadena UTF-8 y se actualiza correctamente esta vez, como se ve en la Figura 4-5.

wadg 0405
Figura 4-5. Lectura de índices y cadenas de la instancia Memory

Como tarea para casa, intenta crear un enfoque aún más flexible que te diga cuántas cadenas hay que leer y cuáles son sus ubicaciones y longitudes. El JavaScript para leerlo puede convertirse en un bucle, y todo el proceso debería ser más flexible.

Hay más cosas que saber sobre las instancias de Memory, como verás más adelante, pero por ahora, hemos cubierto suficientes aspectos básicos de WebAssembly como para que intentar hacer algo más sofisticado a mano en Wat resulte demasiado doloroso. Por tanto, ¡ha llegado el momento de utilizar un lenguaje de alto nivel como C!

1 Un registro es una posición de memoria en el chip que suele proporcionar a una instrucción lo que necesita para ejecutarse.

2 Mi primer ordenador, un Atari 800, empezó con sólo 16 kilobytes de memoria. ¡Fue todo un acontecimiento el día que mi padre llegó a casa con una tarjeta de expansión de 32 kilobytes!

3 Ryan Levick destaca este punto en su debate sobre el interés de Microsoft por Rust.

4 La biblioteca NumPy ayuda a resolver esto reimplementando el almacenamiento homogéneo en matrices C y disponiendo de formas compiladas de las funciones matemáticas para ejecutarse en esas estructuras.

5 Es una referencia a Los viajes de Gulliver, de Jonathan Swift.

6 ¡Buen intento, pero no, Bill Gates nunca lo dijo!

7 Como ejercicio de reflexión, ¿a qué podría llegar $n antes de que se desbordara nuestro tipo de datos i32? ¿Cómo podrías solucionarlo?

8 ¡Es verdad!

Get WebAssembly: La Guía Definitiva now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.