Skip to content

Instantly share code, notes, and snippets.

@zolotyh
Created September 12, 2025 09:19
Show Gist options
  • Save zolotyh/b23fb26f149873aca70aac9507e07b3d to your computer and use it in GitHub Desktop.
Save zolotyh/b23fb26f149873aca70aac9507e07b3d to your computer and use it in GitHub Desktop.

Динамическая загрузка модулей WebAssembly: Практическое руководство

Этот пример демонстрирует создание системы с динамически загружаемыми WASM модулями, где основное приложение может подгружать и использовать дополнительные модули по требованию.

Структура проекта

wasm-dynamic-loading/
├── src/
│   ├── main.cpp          # Основное приложение
│   ├── math_plugin.cpp   # Математический плагин
│   └── text_plugin.cpp   # Текстовый плагин
├── public/
│   └── index.html        # Демо страница
├── build.sh              # Скрипт сборки
└── README.md

Основное приложение

src/main.cpp

#include <emscripten.h>
#include <emscripten/bind.h>
#include <iostream>
#include <string>

class PluginManager {
public:
    // Интерфейс для математических операций
    static int callMathPlugin(const std::string& operation, int a, int b) {
        if (operation == "multiply") {
            return multiply_numbers(a, b);
        } else if (operation == "power") {
            return power_numbers(a, b);
        }
        return 0;
    }
    
    // Интерфейс для текстовых операций
    static std::string callTextPlugin(const std::string& operation, 
                                    const std::string& text) {
        if (operation == "reverse") {
            return reverse_text(text.c_str());
        } else if (operation == "uppercase") {
            return uppercase_text(text.c_str());
        }
        return "";
    }

private:
    // Прототипы функций из динамических модулей
    static int multiply_numbers(int a, int b);
    static int power_numbers(int base, int exponent);
    static const char* reverse_text(const char* text);
    static const char* uppercase_text(const char* text);
};

// Экспортируемые функции для JavaScript
extern "C" {
    EMSCRIPTEN_KEEPALIVE
    int math_operation(const char* op, int a, int b) {
        return PluginManager::callMathPlugin(std::string(op), a, b);
    }
    
    EMSCRIPTEN_KEEPALIVE
    const char* text_operation(const char* op, const char* text) {
        std::string result = PluginManager::callTextPlugin(std::string(op), 
                                                          std::string(text));
        // Возвращаем C-строку (память управляется Emscripten)
        char* cstr = (char*)malloc(result.length() + 1);
        strcpy(cstr, result.c_str());
        return cstr;
    }
    
    EMSCRIPTEN_KEEPALIVE
    void hello_main() {
        printf("🚀 Основное приложение запущено!\n");
    }
}

Математический плагин

src/math_plugin.cpp

#include <emscripten.h>
#include <cmath>

extern "C" {
    EMSCRIPTEN_KEEPALIVE
    int multiply_numbers(int a, int b) {
        printf("📊 Math Plugin: %d × %d = %d\n", a, b, a * b);
        return a * b;
    }
    
    EMSCRIPTEN_KEEPALIVE
    int power_numbers(int base, int exponent) {
        int result = (int)pow(base, exponent);
        printf("📊 Math Plugin: %d^%d = %d\n", base, exponent, result);
        return result;
    }
    
    EMSCRIPTEN_KEEPALIVE
    void math_plugin_info() {
        printf("📊 Математический плагин v1.0 загружен\n");
    }
}

Текстовый плагин

src/text_plugin.cpp

#include <emscripten.h>
#include <string>
#include <algorithm>
#include <cctype>
#include <cstring>

extern "C" {
    EMSCRIPTEN_KEEPALIVE
    const char* reverse_text(const char* input) {
        std::string text(input);
        std::reverse(text.begin(), text.end());
        
        // Выделяем память для результата
        char* result = (char*)malloc(text.length() + 1);
        strcpy(result, text.c_str());
        
        printf("📝 Text Plugin: reversed '%s' -> '%s'\n", input, result);
        return result;
    }
    
    EMSCRIPTEN_KEEPALIVE
    const char* uppercase_text(const char* input) {
        std::string text(input);
        std::transform(text.begin(), text.end(), text.begin(), ::toupper);
        
        char* result = (char*)malloc(text.length() + 1);
        strcpy(result, text.c_str());
        
        printf("📝 Text Plugin: uppercase '%s' -> '%s'\n", input, result);
        return result;
    }
    
    EMSCRIPTEN_KEEPALIVE
    void text_plugin_info() {
        printf("📝 Текстовый плагин v1.0 загружен\n");
    }
}

Скрипт сборки

build.sh

#!/bin/bash

echo "🔨 Начинаем сборку WASM модулей..."

# Создаем директорию для результатов
mkdir -p public/wasm

# Компилируем плагины как side modules
echo "📦 Собираем математический плагин..."
emcc src/math_plugin.cpp -O2 \
    -s SIDE_MODULE=1 \
    -s EXPORT_ALL=1 \
    -o public/wasm/math_plugin.wasm

echo "📦 Собираем текстовый плагин..."
emcc src/text_plugin.cpp -O2 \
    -s SIDE_MODULE=1 \
    -s EXPORT_ALL=1 \
    -o public/wasm/text_plugin.wasm

# Компилируем основное приложение как main module
echo "🏗️ Собираем основное приложение..."
emcc src/main.cpp -O2 \
    -s MAIN_MODULE=1 \
    -s EXPORT_ALL=1 \
    -s MODULARIZE=1 \
    -s EXPORT_NAME="WasmApp" \
    -s RUNTIME_LINKED_LIBS=["public/wasm/math_plugin.wasm","public/wasm/text_plugin.wasm"] \
    -s ALLOW_MEMORY_GROWTH=1 \
    --bind \
    -o public/wasm/main.js

echo "✅ Сборка завершена успешно!"
echo "🌐 Запустите HTTP сервер в директории public/"

HTML демонстрация

public/index.html

<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WASM Dynamic Loading Demo</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .section {
            margin: 20px 0;
            padding: 15px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
        }
        .math-section { border-color: #4CAF50; }
        .text-section { border-color: #2196F3; }
        button {
            background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            padding: 10px 20px;
            margin: 5px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }
        button:hover { opacity: 0.8; }
        input, select {
            padding: 8px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        #output {
            background: #f9f9f9;
            border: 1px solid #ddd;
            padding: 15px;
            border-radius: 5px;
            margin-top: 20px;
            min-height: 100px;
            font-family: 'Courier New', monospace;
            font-size: 14px;
        }
        .loading {
            color: #666;
            font-style: italic;
        }
        .error {
            color: #d32f2f;
            background: #ffebee;
            padding: 10px;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚀 Динамическая загрузка WASM модулей</h1>
        <p>Демонстрация системы с подгружаемыми плагинами на WebAssembly</p>
        
        <div class="section math-section">
            <h3>📊 Математический плагин</h3>
            <label>Операция:</label>
            <select id="mathOp">
                <option value="multiply">Умножение</option>
                <option value="power">Возведение в степень</option>
            </select>
            <input type="number" id="mathA" value="5" placeholder="Число A">
            <input type="number" id="mathB" value="3" placeholder="Число B">
            <button onclick="testMathPlugin()">Выполнить</button>
        </div>
        
        <div class="section text-section">
            <h3>📝 Текстовый плагин</h3>
            <label>Операция:</label>
            <select id="textOp">
                <option value="reverse">Обратить текст</option>
                <option value="uppercase">В верхний регистр</option>
            </select>
            <input type="text" id="textInput" value="Hello WASM!" placeholder="Введите текст">
            <button onclick="testTextPlugin()">Выполнить</button>
        </div>
        
        <button onclick="loadAllPlugins()" style="background: linear-gradient(45deg, #FF6B6B, #4ECDC4);">
            🔄 Загрузить все плагины
        </button>
        
        <div id="output">
            <div class="loading">Ожидание загрузки модулей...</div>
        </div>
    </div>

    <script>
        let wasmModule = null;
        let pluginsLoaded = false;
        
        function log(message, type = 'info') {
            const output = document.getElementById('output');
            const timestamp = new Date().toLocaleTimeString();
            const className = type === 'error' ? 'error' : '';
            
            if (output.innerHTML.includes('Ожидание загрузки')) {
                output.innerHTML = '';
            }
            
            output.innerHTML += `<div class="${className}">[${timestamp}] ${message}</div>`;
            output.scrollTop = output.scrollHeight;
            console.log(message);
        }
        
        // Загрузка основного модуля
        async function initializeWasm() {
            try {
                log('🔄 Загружаем основное приложение...');
                wasmModule = await WasmApp();
                log('✅ Основное приложение загружено');
                wasmModule._hello_main();
                return true;
            } catch (error) {
                log(`❌ Ошибка загрузки: ${error.message}`, 'error');
                return false;
            }
        }
        
        // Загрузка и инициализация плагинов
        async function loadAllPlugins() {
            if (!wasmModule) {
                log('⚠️ Основное приложение еще не загружено', 'error');
                return;
            }
            
            try {
                log('🔄 Загружаем плагины...');
                
                // Имитируем загрузку плагинов
                await new Promise(resolve => setTimeout(resolve, 500));
                
                // Вызываем информационные функции плагинов
                if (wasmModule._math_plugin_info) {
                    wasmModule._math_plugin_info();
                }
                if (wasmModule._text_plugin_info) {
                    wasmModule._text_plugin_info();
                }
                
                pluginsLoaded = true;
                log('✅ Все плагины успешно загружены и готовы к работе!');
                
            } catch (error) {
                log(`❌ Ошибка загрузки плагинов: ${error.message}`, 'error');
            }
        }
        
        // Тестирование математического плагина
        function testMathPlugin() {
            if (!wasmModule || !pluginsLoaded) {
                log('⚠️ Сначала загрузите плагины', 'error');
                return;
            }
            
            const operation = document.getElementById('mathOp').value;
            const a = parseInt(document.getElementById('mathA').value);
            const b = parseInt(document.getElementById('mathB').value);
            
            try {
                const result = wasmModule._math_operation(operation, a, b);
                log(`📊 Результат: ${operation}(${a}, ${b}) = ${result}`);
            } catch (error) {
                log(`❌ Ошибка выполнения: ${error.message}`, 'error');
            }
        }
        
        // Тестирование текстового плагина
        function testTextPlugin() {
            if (!wasmModule || !pluginsLoaded) {
                log('⚠️ Сначала загрузите плагины', 'error');
                return;
            }
            
            const operation = document.getElementById('textOp').value;
            const text = document.getElementById('textInput').value;
            
            try {
                const resultPtr = wasmModule._text_operation(operation, text);
                const result = wasmModule.UTF8ToString(resultPtr);
                log(`📝 Результат: ${operation}("${text}") = "${result}"`);
                
                // Освобождаем память
                wasmModule._free(resultPtr);
            } catch (error) {
                log(`❌ Ошибка выполнения: ${error.message}`, 'error');
            }
        }
        
        // Инициализация при загрузке страницы
        window.addEventListener('load', async () => {
            const success = await initializeWasm();
            if (success) {
                // Автоматически загружаем плагины
                setTimeout(loadAllPlugins, 1000);
            }
        });
    </script>
    
    <script src="wasm/main.js"></script>
</body>
</html>

Инструкции по запуску

  1. Установите Emscripten SDK:

    git clone https://github.com/emscripten-core/emsdk.git
    cd emsdk
    ./emsdk install latest
    ./emsdk activate latest
    source ./emsdk_env.sh
  2. Соберите проект:

    chmod +x build.sh
    ./build.sh
  3. Запустите локальный сервер:

    cd public
    python3 -m http.server 8000
    # или
    npx serve .
  4. Откройте браузер: http://localhost:8000

Ключевые особенности примера

  • Модульная архитектура: Основное приложение + подгружаемые плагины
  • Типизированные интерфейсы: Четкое разделение математических и текстовых операций
  • Управление памятью: Корректная работа с C-строками в WASM
  • Интерактивный UI: Полнофункциональная демонстрация возможностей
  • Обработка ошибок: Надежная система логирования и отладки

Этот пример демонстрирует полный цикл создания приложения с динамической загрузкой WASM модулей и может служить основой для более сложных систем плагинов.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment