В следующем демонстрационном примере я напишу на AssemblyScript несколько функций для работы с простыми числами, а затем опубликую файл индекса в формате WASM. Затем скопирую файл WASM в приложение Angular, потоком отправлю двоичные данные через WebAssembly API и, наконец, стану вызывать эти функции, чтобы с их помощью выполнять различные действия над простыми числами.
Что такое WebAssembly?
WebAssembly – это составное слово, которое делится на 2 части: Web (Веб) и Assembly (Ассемблер). Когда пишешь на высокоуровневых языках, например, на AssemblyScript и Rust, получается код, который компилируется в ассемблер при помощи специальных инструментов. После этого разработчик может нативно выполнять ассемблерный код прямо в браузере, то есть, в вебе.
Как этот демо-пример может использоваться на практике
С этим демо-примером связаны 2 github-репозитория. В первом используется язык AssemblyScript, на котором пишется TypeScript-подобный код, компилируемый в Wasm. Во втором репозитории лежит простое приложение на Angular, в котором при помощи функций Wasm исследуются некоторые любопытные свойства простых чисел.
В индексе репозитория с AssemblyScript содержится 3 функции для работы с простыми числами:
- isPrime – Определить, является ли данное целое число простым
- findFirstNPrimes – Найти первые N простых чисел, где N – целое число
- optimizedSieve – Найти все простые числа менее N, где N — целое число
AssemblyScript долбавляет в package.json скрипты, генерирующие, соответственно, debug.wasm и release.wasm.
Я скопировала release.wasm в каталог assets приложения Angular, написала загрузчик WebAssembly, чтобы организовать потоковую передачу двоичного файла и вернуть экземпляр WebAssembly. Главный компонент связывает экземпляр с компонентами, где он выступает в качестве ввода. Эти компоненты используют экземпляр для выполнения Wasm и вспомогательных функций для получения результатов операций над простыми числами.
Пишем WebAssembly на AssemblyScript
AssemblyScript — это TypeScript-подобный язык, на котором можно писать код, в дальнейшем компилируемый в WebAssembly.
Начинаем новый проект
npm init
Устанавливаем зависимость
npm install --save-dev assemblyscript
Выполняем команду для добавления скриптов в package.json и файлов скаффолда
npx asinit .
Наши собственные скрипты для генерации файлов debug.wasm и release.wasm
"scripts": { "asbuild:debug": "asc assembly/index.ts --target debug --exportRuntime", "asbuild:release": "asc assembly/index.ts --target release --exportRuntime", "asbuild": "npm run asbuild:debug && npm run asbuild:release", "start": "npx serve ." }
Реализуем алгоритм для работы над простыми числами на AssemblyScript
// assembly/index.ts // Запись в файле, в этой записи указан модуль WebAssembly // импорт модуля declare function primeNumberLog(primeNumber: i32): void; export function isPrime(n: i32): bool { if (n <= 1) { return false; } else if (n === 2 || n === 3) { return true; } else if (n % 2 === 0 || n % 3 === 0) { return false; } for (let i = 5; i <= Math.sqrt(n); i = i + 6) { if (n % i === 0 || n % (i + 2) === 0) { return false; } } return true; } export function findFirstNPrimes(n: i32): Array<i32> { let primes = new Array<i32>(n); for (let i = 0; i < n; i++) { primes[i] = 0; } primes[0] = 2; primeNumberLog(primes[0]); let num = 3; let index = 0; while(index < n - 1) { let isPrime = true; for (let i = 0; i <= index; i++) { if (num % primes[i] === 0) { isPrime = false; break; } } if (isPrime) { primeNumberLog(num); primes[index + 1] = num; index = index + 1; } num = num + 2; } return primes; } const MAX_SIZE = 1000001; export function optimizedSieve(n: i32): Array<i32> { const isPrime = new Array<bool>(MAX_SIZE); isPrime.fill(true, 0, MAX_SIZE); const primes = new Array<i32>(); const smallestPrimeFactors = new Array<i32>(MAX_SIZE); smallestPrimeFactors.fill(1, 0, MAX_SIZE); isPrime[0] = false; isPrime[1] = false; for (let i = 2; i < n; i++) { if (isPrime[i]) { primes.push(i); smallestPrimeFactors[i] = i; } for (let j = 0; j < primes.length && i * primes[j] < n && primes[j] <= smallestPrimeFactors[i]; j++) { const nonPrime = i * primes[j]; isPrime[nonPrime] = false; smallestPrimeFactors[nonPrime] = primes[j]; } } const results = new Array<i32>(); for (let i = 0; i < primes.length && primes[i] <= n; i++) { results.push(primes[i]); } return results; }
primeNumberLog — это внешняя функция, которая логирует простые числа в findFirstNPrimes. У этой функции нет тела, и именно приложение Angular отвечает за то, чтобы предоставить для неё детали реализации.
После выполнения скрипта npm run asbuild в каталоге builds/ будут содержаться файлы debug.wasm и release.wasm. Та часть работы, которая касается WebAssembly, уже готова, и далее мы будем работать только с приложением Angular.
Комбинируем сильные стороны WebAssembly и Angular
В WebAssembly не переносятся такие высокоуровневые типы данных, как, например, массивы и булевы значения. Поэтому я установила загрузчик assemblyscript и с помощью содержащихся в нём вспомогательных функций привожу к корректным типам значения, возвращаемые функциями Wasm.
Установка зависимости
npm i @assemblyscript/loader
Сборка загрузчика WebAssembly
Далее, методом проб и ошибок я смогла импортировать в приложение Angular функции Wasm, организовав потоковую передачу release.wasm при помощи загрузчика assemblyscript.
src ├── assets │ └── release.wasm ├── favicon.ico ├── index.html ├── main.ts └── styles.scss
Я инкапсулировала загрузчик WebAssembly в загрузочный сервис, тем самым позволив всем компонентам Angular многократно использовать функционал потоковой передачи данных. Если в браузере поддерживается функция instantiateStreaming, то возвращается экземпляр WebAssembly. Если функция instantiateStreaming не поддерживается, то будет вызвана резервная функция. Резервная функция преобразует ответ в массив ArrayBuffer и собирает экземпляр WebAssembly.
DEFAULT_IMPORTS также предоставляет реализацию primeNumberLog. primeNumberLog объявляется в файле index.ts репозитория AssemblyScript. Следовательно, ключом объекта служит его позиция в индексе без файлового расширения.
// web-assembly-loader.service.ts import { Injectable } from '@angular/core'; import loader, { Imports } from '@assemblyscript/loader'; const DEFAULT_IMPORTS: Imports = { env: { abort: function() { throw new Error('Abort called from wasm file'); }, }, index: { primeNumberLog: function(primeNumber: number) { console.log(`primeNumberLog: ${primeNumber}`); } } } @Injectable({ providedIn: 'root' }) export class WebAssemblyLoaderService { async streamWasm(wasm: string, imports = DEFAULT_IMPORTS): Promise<any> { if (!loader.instantiateStreaming) { return this.wasmFallback(wasm, imports); } const instance = await loader.instantiateStreaming(fetch(wasm), imports); return instance?.exports; } async wasmFallback(wasm: string, imports: Imports) { console.log('using fallback'); const response = await fetch(wasm); const bytes = await response?.arrayBuffer(); const { instance } = await loader.instantiate(bytes, imports); return instance?.exports; } }
Связываем экземпляр WebAssembly с компонентами Angular
В AppComponent я потоком передала release.wasm, чтобы собрать экземпляр WebAssembly. Затем я связала этот экземпляр с выводом из Angular Components.
// app.config.ts export const appConfig: ApplicationConfig = { providers: [ { provide: APP_BASE_HREF, useFactory: () => inject(PlatformLocation).getBaseHrefFromDOM(), } ] }; // full-asset-path.ts export const getFullAssetPath = (assetName: string) => { const baseHref = inject(APP_BASE_HREF); const isEndWithSlash = baseHref.endsWith('/'); return `${baseHref}${isEndWithSlash ? '' : '/'}assets/${assetName}`; } // app.component.ts @Component({ selector: 'app-root', standalone: true, imports: [FormsModule, IsPrimeComponent, FindFirstNPrimesComponent, OptimizedSieveComponent], template: ` <div class="container outer" style="margin: 0.5rem;"> <h2>Angular + WebAssembly Demo</h2> <app-is-prime [instance]="instance" /> <app-find-first-nprimes [instance]="instance" /> <app-optimized-sieve [instance]="instance" /> </div> `, }) export class AppComponent implements OnInit { instance!: any; releaseWasm = getFullAssetPath('release.wasm'); wasmLoader = inject(WebAssemblyLoaderService); async ngOnInit(): Promise<void> { this.instance = await this.wasmLoader.streamWasm(this.releaseWasm); console.log(this.instance); } }
Применение WebAssembly к Angular Components
IsPrimeComponent вызывает функцию isPrime, определяющую, является ли данное целое число простым. isPrime возвращает 1, если перед нами простое число, в противном случае возвращает 0. Следовательно, оператор === сравнивает целочисленные значения, чтобы в результате вернуть булево.
// is-prime.component.ts @Component({ selector: 'app-is-prime', standalone: true, imports: [FormsModule], template: ` <form> <label for="primeNumber"> <span>Input an positive integer: </span> <input id="primeNumber" name="primeNumber" type="number" [ngModel]="primeNumber()" (ngModelChange)="primeNumber.set($event)" /> </label> </form> <p class="bottom-margin">isPrime({{ primeNumber() }}): {{ isPrimeNumber() }}</p> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class IsPrimeComponent { @Input({ required: true }) instance!: any; primeNumber = signal(0); isPrimeNumber = computed(() => { const value = this.primeNumber(); return this.instance ? this.instance.isPrime(value) === 1 : false }); }
FindFirstNPrimesComponent вызывает функцию findFirstNPrimes, чтобы получить первые N простых чисел. Функция findFirstNPrimes не может перенести целочисленный массив, поэтому я пользуюсь вспомогательной функцией __getArray, имеющейся в загрузчике, и с её помощью преобразую целочисленное значение в корректный целочисленный массив.
// find-first-nprimes.component.ts @Component({ selector: 'app-find-first-nprimes', standalone: true, imports: [FormsModule], template: ` <form> <label for="firstNPrimeNumbers"> <span>Find first N prime numbers: </span> <input id="firstNPrimeNumbers" name="firstNPrimeNumbers" type="number" [ngModel]="firstN()" (ngModelChange)="firstN.set($event)" /> </label> </form> <p class="bottom-margin">First {{ firstN() }} prime numbers:</p> <div class="container first-n-prime-numbers bottom-margin"> @for(primeNumber of firstNPrimeNumbers(); track primeNumber) { <span style="padding: 0.25rem;">{{ primeNumber }}</span> } <div> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class FindFirstNPrimesComponent { @Input({ required: true }) instance!: any; firstN = signal(0); firstNPrimeNumbers = computed(() => { const value = this.firstN(); if (this.instance) { const { findFirstNPrimes, __getArray: getArray } = this.instance; return getArray(findFirstNPrimes(value)); } return []; }); }
OptimizedSieveComponent вызывает функцию optimizedSieve, чтобы получить все простые числа, которые меньше to obtain N. Функция optimizedSieve также не может перенести целочисленный массив, и я пользуюсь вспомогательной функцией __getArray, с её помощью преобразую целочисленное значение в корректный целочисленный массив.
// optimized-sieve.component.ts @Component({ selector: 'app-optimized-sieve', standalone: true, imports: [FormsModule], template: ` <form> <label for="primeNumber"> <span>Input an positive integer: </span> <input id="primeNumber" name="primeNumber" type="number" [ngModel]="lessThanNumber()" (ngModelChange)="lessThanNumber.set($event)" /> </label> </form> <p class="bottom-margin">Prime numbers less than {{ lessThanNumber() }}</p> <div class="container prime-numbers-less-than-n bottom-margin"> @for(primeNumber of primeNumbers(); track primeNumber) { <span style="padding: 0.25rem;">{{ primeNumber }}</span> } <div> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class OptimizedSieveComponent { @Input({ required: true }) instance!: any; lessThanNumber = signal(0); primeNumbers = computed(() => { const value = this.lessThanNumber(); if (this.instance) { const { optimizedSieve, __getArray: getArray } = this.instance; return getArray(optimizedSieve(value)); } return []; }); }
Готовый пример показан на следующей странице:
railsstudent.github.io
Вот и всё. Надеюсь, вам понравился этот пост, и он вдохновит вас на изучение Angular и других технологий.
Ресурсы:
- Репозиторий с WebAssembly: github.com/railsstudent/prime-number-wasm
- Репозиторий с демо-примером на Angular + WebAssembly: github.com/railsstudent/ng-webassembly-demo
- Интерактивное демо: railsstudent.github.io/ng-webassembly-demo
- AssemblyScript: angular.dev/guide/templates/control-flow#if-block-conditionals
- WebAssembly API: developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming_static
ссылка на оригинал статьи https://habr.com/ru/articles/796329/
Добавить комментарий