Lesson 11-WebAssembly Source Code Architecture

WebAssembly Binary Format Source Code

Module Sections Analysis (Type, Function, Code, Memory, etc.)

The WebAssembly binary format uses a segmented structure, with each section serving a specific function and encoding method.

Core Section Types:

  1. Type Section: Defines function signatures
    • Encoding format: vec(func_type), where func_type is (param_types) -> (result_types)
    • Source implementation typically includes type table construction and validation
  2. Function Section: Declares functions in the module
    • Encoding format: vec(type_idx), indices pointing to type definitions in the Type section
    • Requires mapping function indices to type definitions during implementation
  3. Code Section: Contains function body implementations
    • Encoding format: vec(code), each code structure includes local variable declarations and instruction sequences
    • Source code needs to implement instruction decoding and local variable management
  4. Memory Section: Defines linear memory
    • Encoding format: vec(memory_type), where memory_type is limits(min, max?)
    • Implementation requires handling memory initialization and growth logic

Source Code Example (Simplified):

// Pseudo-code for parsing Type section
void parse_type_section(BinaryReader& reader) {
    uint32_t count = reader.read_varuint32();
    for (uint32_t i = 0; i < count; i++) {
        uint32_t param_count = reader.read_varuint32();
        std::vector<ValType> params;
        for (uint32_t j = 0; j < param_count; j++) {
            params.push_back(read_valtype(reader));
        }

        uint32_t result_count = reader.read_varuint32();
        std::vector<ValType> results;
        for (uint32_t j = 0; j < result_count; j++) {
            results.push_back(read_valtype(reader));
        }

        type_table.push_back({params, results});
    }
}

Binary Encoding and Decoding (LEB128 Encoding)

WebAssembly uses LEB128 (Little Endian Base 128) variable-length integer encoding to compress data.

LEB128 Characteristics:

  • Each byte uses 7 bits for data, with the most significant bit as a continuation flag
  • Supports signed and unsigned integer encoding

Decoding Implementation Example:

// Pseudo-code for LEB128 decoding
uint32_t decode_u32_leb128(BinaryReader& reader) {
    uint32_t result = 0;
    uint32_t shift = 0;
    uint8_t byte;

    do {
        byte = reader.read_byte();
        result |= (byte & 0x7f) << shift;
        shift += 7;
    } while (byte & 0x80);

    return result;
}

Special Handling in Source Code:

  • Boundary checks to prevent maliciously crafted overly long encodings
  • Performance optimization: Batch reading and decoding
  • Error handling: Detection of invalid encodings

Instruction Set Implementation (Numeric Operations, Control Flow, Memory Access)

The WebAssembly instruction set is divided into several categories:

  1. Numeric Operation Instructions:
    • Integer operations: i32.add, i64.mul, etc.
    • Floating-point operations: f32.sqrt, f64.copysign, etc.
    • Conversion instructions: i32.wrap/i64, f32.demote/f64, etc.
  2. Control Flow Instructions:
    • Unconditional jumps: br, br_table
    • Conditional branches: if, else, end
    • Loops: loop, block
  3. Memory Access Instructions:
    • Load: i32.load, f64.load
    • Store: i32.store, f64.store
    • Memory size management: memory.size, memory.grow

Instruction Execution Engine Example:

// Pseudo-code for instruction dispatch
void execute_instruction(VMContext& ctx, Opcode opcode) {
    switch (opcode) {
        case Opcode::I32_ADD:
            {
                int32_t a = ctx.pop_i32();
                int32_t b = ctx.pop_i32();
                ctx.push_i32(a + b);
            }
            break;

        case Opcode::I32_LOAD:
            {
                uint32_t offset = ctx.pop_u32();
                uint32_t align = /* Parsed from instruction */;
                uint32_t addr = ctx.pop_u32() + offset;
                int32_t value = ctx.memory.read_i32(addr, align);
                ctx.push_i32(value);
            }
            break;

        // Other instructions...
    }
}

Module Validation and Loading Process

Module validation is a critical step to ensure the safety of WASM binaries.

Validation Process:

  1. Structural Validation: Check section order and basic structure
  2. Type Validation: Verify function signature consistency
  3. Instruction Validation: Check operand types and stack balance
  4. Memory Validation: Ensure memory accesses are within bounds

Source Implementation Key Points:

  • Use a state machine to track the validation process
  • Maintain type stack and operand stack simulation
  • Terminate loading of invalid modules early

Validation Pseudo-code:

bool validate_module(const Module& module) {
    // 1. Check if required sections exist
    if (!module.has_type_section()) return false;

    // 2. Verify function type consistency
    for (auto func_idx : module.function_indices()) {
        auto expected_type = module.type_at(func_idx);
        if (!check_function_type(module, func_idx, expected_type)) {
            return false;
        }
    }

    // 3. Validate code section
    for (auto& code : module.codes()) {
        if (!validate_code(code)) {
            return false;
        }
    }

    return true;
}

Binary Format Extensions (SIMD, Threads, Exception Handling)

WebAssembly supports new features through extensions.

Main Extensions:

  1. SIMD (Single Instruction Multiple Data):
    • Introduces v128 type
    • 128-bit vector operation instructions
    • Requires extending the type system and instruction set
  2. Threads:
    • Shared memory support
    • Atomic operation instructions
    • Requires thread-safe memory access implementation
  3. Exception Handling:
    • try/catch/throw instructions
    • Exception type system
    • Requires modifications to control flow handling logic

Extension Implementation Example:

// Pseudo-code for SIMD instruction handling
void execute_simd_instruction(VMContext& ctx, SimdOpcode opcode) {
    switch (opcode) {
        case SimdOpcode::V128_LOAD:
            {
                uint32_t offset = ctx.pop_u32();
                uint32_t align = /* Parse alignment */;
                uint32_t addr = ctx.pop_u32() + offset;
                v128_t value = ctx.memory.read_v128(addr, align);
                ctx.push_v128(value);
            }
            break;

        // Other SIMD instructions...
    }
}

WebAssembly Virtual Machine Source Code

Stack-Based Virtual Machine Implementation (Operand Stack, Control Stack)

WebAssembly uses a stack-based virtual machine design.

Core Data Structures:

  1. Operand Stack: Stores intermediate computation values
  2. Control Stack: Manages control flow contexts (blocks, loops, functions)

Stack Frame Structure Example:

struct StackFrame {
    uint32_t return_pc;    // Return address
    uint32_t local_count;  // Number of local variables
    ValType* local_types;  // Local variable types
    // May include other frame-specific data
};

class VMContext {
    std::vector<Value> operand_stack;  // Operand stack
    std::vector<StackFrame> call_stack; // Control stack
    // Other VM state...
};

Stack Operation Implementation:

void push_value(VMContext& ctx, const Value& val) {
    ctx.operand_stack.push_back(val);
}

Value pop_value(VMContext& ctx) {
    if (ctx.operand_stack.empty()) {
        throw RuntimeError("Operand stack underflow");
    }
    auto val = ctx.operand_stack.back();
    ctx.operand_stack.pop_back();
    return val;
}

Instruction Execution and Scheduling

Instruction execution is the core functionality of the virtual machine.

Execution Loop Pseudo-code:

void run(VMContext& ctx) {
    while (ctx.pc < ctx.code_size) {
        Opcode opcode = read_opcode(ctx);

        // Performance optimization: Use jump table instead of switch
        static void (*dispatch_table[])(VMContext&) = {
            /* Opcode::UNREACHABLE */ handle_unreachable,
            /* Opcode::NOP */ handle_nop,
            /* Opcode::BLOCK */ handle_block,
            // ...Other instruction handlers
        };

        dispatch_table[opcode](ctx);
        ctx.pc += get_opcode_length(opcode);
    }
}

Instruction Scheduling Optimizations:

  • Use jump tables instead of large switch statements
  • Batch prefetch instructions
  • Optimize hot paths

Memory Management and Bounds Checking

Memory safety is a core feature of WASM.

Memory Access Implementation:

Value memory_read(VMContext& ctx, uint32_t addr, ValType type, uint32_t align) {
    // Bounds checking
    if (addr + size_of(type) > ctx.memory.size()) {
        throw RuntimeError("Memory access out of bounds");
    }

    // Alignment checking
    if (addr % align != 0) {
        throw RuntimeError("Unaligned memory access");
    }

    // Actual read
    switch (type) {
        case ValType::I32:
            return Value(ctx.memory.read_i32(addr, align));
        case ValType::F64:
            return Value(ctx.memory.read_f64(addr, align));
        // ...Other types
    }
}

Memory Growth Implementation:

uint32_t memory_grow(VMContext& ctx, uint32_t delta) {
    uint32_t old_size = ctx.memory.size() / PAGE_SIZE;
    uint32_t new_size = old_size + delta;

    if (new_size > MAX_MEMORY_PAGES) {
        return UINT32_MAX; // Indicates growth failure
    }

    if (!ctx.memory.grow(delta * PAGE_SIZE)) {
        return UINT32_MAX;
    }

    return old_size;
}

Function Calls and Table Management

Function calls involve parameter passing and return value handling.

Function Call Implementation:

void call_function(VMContext& ctx, uint32_t func_idx) {
    // 1. Get function reference
    auto& func = ctx.module.functions[func_idx];

    // 2. Verify parameter count
    if (ctx.operand_stack.size() < func.param_count) {
        throw RuntimeError("Insufficient arguments");
    }

    // 3. Create new stack frame
    StackFrame frame;
    frame.return_pc = ctx.pc + get_opcode_length(current_opcode);
    frame.local_count = func.local_count;
    frame.local_types = func.local_types;

    ctx.call_stack.push_back(frame);

    // 4. Set parameters
    for (uint32_t i = 0; i < func.param_count; i++) {
        ctx.frame_locals[i] = ctx.pop_value();
    }

    // 5. Jump to function entry
    ctx.pc = func.start_pc;
}

Table Management Implementation:

uint32_t table_get(VMContext& ctx, uint32_t idx) {
    if (idx >= ctx.module.table.initial) {
        throw RuntimeError("Table index out of bounds");
    }

    return ctx.table.refs[idx];
}

void table_set(VMContext& ctx, uint32_t idx, uint32_t ref) {
    if (idx >= ctx.module.table.initial) {
        throw RuntimeError("Table index out of bounds");
    }

    ctx.table.refs[idx] = ref;
}

Exception Handling and Error Propagation

The exception handling mechanism ensures errors are propagated correctly.

Exception Handling Implementation:

void handle_throw(VMContext& ctx) {
    uint32_t exception_idx = ctx.pop_u32();
    auto& exception = ctx.module.exceptions[exception_idx];

    // Collect parameters
    std::vector<Value> args;
    for (uint32_t i = 0; i < exception.param_count; i++) {
        args.push_back(ctx.pop_value());
    }
    std::reverse(args.begin(), args.end());

    // Find catch block
    for (auto it = ctx.call_stack.rbegin(); it != ctx.call_stack.rend(); ++it) {
        if (it->has_catch(exception_idx)) {
            // Set exception context and jump to catch block
            ctx.current_exception = {exception_idx, args};
            ctx.pc = it->catch_pc;
            return;
        }
    }

    // No catch block found, terminate execution
    throw RuntimeException("Uncaught exception");
}

WebAssembly Toolchain Source Code

Emscripten Compiler Source Code (C/C++ to WASM Conversion)

Emscripten compiles C/C++ code into WASM.

Key Components:

  1. Frontend: Clang/LLVM processes C/C++ code
  2. Backend: Emscripten-specific LLVM passes
  3. Runtime: Provides system emulation (file system, network, etc.)

Compilation Process Pseudo-code:

void compile_to_wasm(const std::string& source) {
    // 1. Use Clang to generate LLVM IR
    auto ir = clang_compile_to_ir(source);

    // 2. Apply Emscripten-specific optimizations
    apply_emscripten_optimizations(ir);

    // 3. Generate WASM
    auto wasm = llvm_generate_wasm(ir);

    // 4. Add runtime support
    add_runtime_support(wasm);

    // 5. Output final WASM file
    write_wasm_file(wasm);
}

System Call Emulation:

// Pseudo-code for file system emulation
void emulate_fs_call(WASMContext& ctx, uint32_t syscall_num, uint32_t* args) {
    switch (syscall_num) {
        case FS_OPEN:
            {
                const char* path = ctx.memory.read_string(args[0]);
                int flags = args[1];
                int fd = fs_open(path, flags);
                ctx.set_return_value(fd);
            }
            break;

        // Other system calls...
    }
}

LLVM Backend Source Code (WASM Target Support)

The LLVM backend converts IR to WASM.

Key Implementations:

  1. WASM Target Description: Defines instruction set and ABI
  2. Code Generator: Converts LLVM IR to WASM instructions
  3. Optimization Passes: WASM-specific optimizations

Instruction Selection Example:

// Pseudo-code for instruction selection
void select_instruction(Instruction* inst) {
    switch (inst->opcode()) {
        case Instruction::Add:
            if (inst->type()->is_integer_ty()) {
                // Generate i32.add or i64.add
                emit_wasm_insn(inst->is_32bit() ? WASM_I32_ADD : WASM_I64_ADD);
            } else if (inst->type()->is_floating_point_ty()) {
                // Generate f32.add or f64.add
                emit_wasm_insn(inst->is_32bit() ? WASM_F32_ADD : WASM_F64_ADD);
            }
            break;

        // Other instructions...
    }
}

Memory Model Implementation:

// Pseudo-code for memory access generation
void generate_memory_access(MemoryAccessInst* inst) {
    // Calculate address
    Value* addr = calculate_address(inst);

    // Generate load/store instructions
    if (inst->is_load()) {
        switch (inst->type()->get_primitive_size_in_bits()) {
            case 32:
                emit_wasm_insn(WASM_I32_LOAD);
                break;
            case 64:
                emit_wasm_insn(WASM_I64_LOAD);
                break;
            // Other types...
        }
    } else {
        // Store instructions...
    }
}

wasm-pack Source Code (Rust to WASM Packaging)

wasm-pack is the official Rust WASM toolchain.

Core Functions:

  1. Project Initialization: Creates basic project structure
  2. Build System Integration: Integrates with Cargo
  3. Packaging: Generates WASM packages for different environments
  4. JavaScript Glue Code Generation

Build Process Pseudo-code:

// Pseudo-code for build process
fn build_wasm_project(project_dir: &Path) -> Result<()> {
    // 1. Read Cargo.toml configuration
    let config = read_cargo_toml(project_dir)?;

    // 2. Set build target to wasm32-unknown-unknown
    set_cargo_target("wasm32-unknown-unknown")?;

    // 3. Execute Cargo build
    let build_result = cargo_build(project_dir)?;

    // 4. Process generated WASM file
    let wasm_file = find_wasm_file(build_result.output_dir)?;

    // 5. Apply wasm-opt optimization
    optimize_wasm(wasm_file)?;

    // 6. Generate JavaScript glue code
    generate_js_glue(project_dir, wasm_file)?;

    // 7. Create pkg directory structure
    create_pkg_directory(project_dir)?;

    // 8. Copy necessary files to pkg directory
    copy_files_to_pkg(project_dir)?;

    Ok(())
}

JavaScript Glue Code Generation:

// Pseudo-code for JS glue code generation
fn generate_js_glue(project_dir: &Path, wasm_file: &Path) -> Result<()> {
    let mut js_code = String::new();

    // 1. Generate module loading code
    js_code.push_str("// Auto-generated WASM glue code\n");
    js_code.push_str("import * as wasm from './");
    js_code.push_str(wasm_file.file_name().unwrap().to_str().unwrap());
    js_code.push_str("';\n\n");

    // 2. Generate export function wrappers
    for export in get_wasm_exports(wasm_file)? {
        js_code.push_str(&format!("export function {}(", export.name));
        // Parameter list...
        js_code.push_str(") {\n");
        js_code.push_str(&format!("    return wasm.{}(", export.name));
        // Parameter passing...
        js_code.push_str(");\n");
        js_code.push_str("}\n\n");
    }

    // 3. Write file
    let js_path = project_dir.join("pkg").join("index.js");
    fs::write(js_path, js_code)?;

    Ok(())
}

wasm-bindgen Source Code (JavaScript and WASM Interaction)

wasm-bindgen simplifies interactions between Rust and JavaScript.

Core Functions:

  1. Type Conversion: Converts between Rust and JavaScript types
  2. Function Binding: Automatically generates function bindings
  3. DOM Access: Provides safe DOM operation interfaces

Type Conversion Implementation:

// Pseudo-code for type conversion
fn convert_to_js(value: &Value) -> JsValue {
    match value {
        Value::I32(i) => JsValue::from(*i),
        Value::F64(f) => JsValue::from(*f),
        Value::String(s) => JsValue::from_str(s),
        Value::Array(arr) => {
            let js_array = JsValue::new(js_sys::Array::new());
            for item in arr {
                js_array.push(&convert_to_js(item));
            }
            js_array
        }
        // Other types...
    }
}

Function Binding Generation:

// Pseudo-code for function binding generation
fn generate_function_binding(func: &Function) -> String {
    let mut js_code = String::new();

    // 1. Generate function signature
    js_code.push_str(&format!("export function {}(", func.name));
    for param in &func.params {
        js_code.push_str(¶m.js_type());
        js_code.push_str(", ");
    }
    if !func.params.is_empty() {
        js_code.truncate(js_code.len() - 2); // Remove trailing ", "
    }
    js_code.push_str(") {\n");

    // 2. Generate parameter conversion
    for (i, param) in func.params.iter().enumerate() {
        js_code.push_str(&format!("    let {} = {};\n", 
            param.rust_name(), 
            param.js_to_rust_conversion(i + 1)));
    }

    // 3. Generate function call
    js_code.push_str(&format!("    let result = wasm.{}(", func.name));
    for i in 0..func.params.len() {
        if i > 0 { js_code.push_str(", "); }
        js_code.push_str(&format!("{}{}", 
            if param.needs_conversion() { "convert_to_wasm(" } else { "" },
            param.rust_name(),
            if param.needs_conversion() { ")" } else { "" }));
    }
    js_code.push_str(");\n");

    // 4. Generate return value conversion
    if let Some(return_type) = &func.return_type {
        js_code.push_str(&format!("    return {};\n", 
            return_type.rust_to_js_conversion("result")));
    } else {
        js_code.push_str("    return;\n");
    }

    js_code.push_str("}\n");
    js_code
}

WABT Tool Source Code (WAT and WASM Conversion)

The WebAssembly Binary Toolkit (WABT) provides tools for converting between WASM binary and text formats.

Core Components:

  1. WASM Parser: Reads and validates WASM binaries
  2. WAT Generator: Generates readable text from binaries
  3. WASM Generator: Generates binaries from text
  4. Validator: Ensures modules comply with specifications

WASM to WAT Conversion Pseudo-code:

void binary_to_text(const Module& module, TextWriter& writer) {
    // 1. Write module start
    writer.write("(module\n");

    // 2. Process sections
    for (const auto& type : module.types) {
        writer.write("  (type ");
        write_func_type(writer, type);
        writer.write(")\n");
    }

    for (const auto& func : module.functions) {
        writer.write("  (func ");
        // Write function signature index and local variables
        // ...
        writer.write("\n");

        // Write instructions
        for (const auto& instr : func.code) {
            writer.write("    ");
            write_instruction(writer, instr);
            writer.write("\n");
        }

        writer.write("  )\n");
    }

    // 3. Write module end
    writer.write(")\n");
}

WAT to WASM Conversion Pseudo-code:

void text_to_binary(TextParser& parser, ModuleBuilder& builder) {
    while (parser.has_more()) {
        auto token = parser.next_token();

        if (token == "(type") {
            // Parse function type
            FuncType type = parse_func_type(parser);
            builder.add_type(type);
        }
        else if (token == "(func") {
            // Parse function
            Function func = parse_function(parser);
            builder.add_function(func);
        }
        // Other section processing...
    }

    // Validate module
    if (!builder.validate()) {
        throw ParseError("Invalid module");
    }

    // Generate binary
    builder.build_binary();
}

Instruction Parsing Example:

Instruction parse_instruction(TextParser& parser) {
    auto opcode_token = parser.next_token();
    Opcode opcode = parse_opcode(opcode_token);

    switch (opcode) {
        case Opcode::I32_ADD:
            return Instruction(Opcode::I32_ADD);

        case Opcode::I32_LOAD: {
            // Parse memory operands
            uint32_t align = parse_align(parser);
            uint32_t offset = parse_offset(parser);
            return Instruction(Opcode::I32_LOAD, align, offset);
        }

        // Other instructions...
    }
}

Summary

The WebAssembly source architecture demonstrates its strength as a foundation for modern web technologies:

  1. Binary Format: A carefully designed compact binary format supporting multiple extensions
  2. Virtual Machine Implementation: An efficient and secure stack-based execution environment
  3. Toolchain Ecosystem: A comprehensive toolchain from high-level languages to browsers

Each component is meticulously designed to balance performance, security, and usability, making WebAssembly an ideal choice for web platforms and high-performance computing.

Share your love