Skip to main content

Overview

2c (“to C”) is Porffor’s own WebAssembly-to-C compiler. It translates the WebAssembly bytecode generated by Porffor into optimized C code, which can then be compiled into native binaries using standard C compilers. Unlike traditional Wasm-to-C tools like wasm2c, 2c uses Porffor’s internal information to generate specific, efficient C code with minimal boilerplate.
2c is experimental. Native binaries are not sandboxed and haven’t been proven to be fully safe. Use with caution, especially with --secure mode enabled.

Why 2c?

Benefits:
  • Native Performance: Compiled binaries run at near-native C speeds
  • No Runtime: Zero runtime overhead, no JIT compilation
  • Standalone: Single binary with no external dependencies
  • Cross-Platform: Compile on one platform, run on another
  • Small Footprint: Minimal boilerplate code
Tradeoffs:
  • Not sandboxed (unlike WebAssembly)
  • Experimental and may have bugs
  • Larger binary size than pure Wasm
  • Compile time is longer

Compiling to Native Binaries

Compile JavaScript directly to a native executable:
porf native script.js output

Platform-Specific Output

# Linux/macOS
porf native script.js myapp
# Creates: myapp

# Windows
porf native script.js myapp.exe
# Creates: myapp.exe

Choosing a C Compiler

Specify which C compiler to use:
# Clang (default, recommended)
porf native script.js output --compiler=clang

# GCC
porf native script.js output --compiler=gcc

# Zig (cross-compilation friendly)
porf native script.js output --compiler=zig

Optimization Levels

Control C compiler optimization:
# Ofast (default) - Maximum speed, may break strict standards
porf native script.js output --cO=Ofast

# O3 - Maximum standards-compliant optimization
porf native script.js output --cO=O3

# O2 - Moderate optimization
porf native script.js output --cO=O2

# O1 - Light optimization
porf native script.js output --cO=O1

# O0 - No optimization (for debugging)
porf native script.js output --cO=O0
Binaries are stripped by default to reduce size. Debug symbols are removed.

Compiling to C Source

Generate C code without compiling to binary:
# Write to file
porf c script.js output.c

# Print to stdout
porf c script.js
This is useful for:
  • Inspecting generated C code
  • Custom compilation workflows
  • Understanding how 2c works
  • Debugging issues

Example Output

porf c hello.js hello.c
Input (hello.js):
console.log('Hello, World!');
Output (hello.c) (simplified):
typedef uint8_t u8;
typedef uint16_t u16;
typedef int32_t i32;
typedef uint32_t u32;
typedef int64_t i64;
typedef uint64_t u64;
typedef float f32;
typedef double f64;

const f64 NaN = 0e+0/0e+0;
const f64 Infinity = 1e+0/0e+0;

struct ReturnValue {
  f64 value;
  i32 type;
};

char* _memory;
u32 _memoryPages = 1;

// ... generated functions ...

int main(int argc, char** argv) {
  _memory = calloc(1, _memoryPages * 65536);
  // ... initialization ...
  // ... your code ...
  return 0;
}

How 2c Works

Compilation Pipeline

JavaScript → Porffor Compiler → WebAssembly Bytecode

                                   2c Compiler

                                    C Source

                                C Compiler (clang/gcc/zig)

                                 Native Binary

Type System

2c translates WebAssembly types to C types:
Wasm TypeC TypeDescription
i32i32 (int32_t)32-bit integer
u32u32 (uint32_t)Unsigned 32-bit integer
i64i64 (int64_t)64-bit integer
u64u64 (uint64_t)Unsigned 64-bit integer
f32f32 (float)32-bit float
f64f64 (double)64-bit float
i8u8 (uint8_t)8-bit integer
i16u16 (uint16_t)16-bit integer

Memory Model

2c allocates WebAssembly linear memory using C’s heap:
char* _memory;
u32 _memoryPages = 4; // Example: 4 pages = 256KB

int main(int argc, char** argv) {
  _memory = calloc(1, _memoryPages * 65536); // 64KB per page
  // ...
}
Memory operations are translated directly:
// WebAssembly: i32.store
*((i32*)(_memory + offset + pointer)) = value;

// WebAssembly: i32.load
return *((i32*)(_memory + offset + pointer));

// WebAssembly: i32.store8 (byte)
*((u8*)(_memory + offset + pointer)) = value;

// WebAssembly: i32.load8_u (unsigned byte)
return *((u8*)(_memory + offset + pointer));

Memory Safety Option

Use memcpy instead of pointer casts (safer but potentially slower):
porf native script.js output --2cMemcpy
With --2cMemcpy:
// Store
memcpy(_memory + offset + pointer, &value, sizeof(value));

// Load
i32 out;
memcpy(&out, _memory + offset + pointer, sizeof(out));
return out;
This can help avoid alignment issues on some architectures.

Function Translation

2c translates WebAssembly functions to C functions: WebAssembly:
(func $add (param $a f64) (param $b f64) (result f64)
  local.get $a
  local.get $b
  f64.add)
Generated C:
f64 add(f64 a, f64 b) {
  return a + b;
}

Multi-Value Returns

Porffor functions typically return (value, type) pairs:
struct ReturnValue {
  f64 value;
  i32 type;
};

struct ReturnValue myFunction(f64 arg) {
  struct ReturnValue ret;
  ret.value = 42.0;
  ret.type = 1; // TYPES.number
  return ret;
}

Platform-Specific Code

2c can generate platform-specific code for Windows vs. Unix:
#ifdef _WIN32
  // Windows-specific code
  #include <windows.h>
#else
  // Unix-specific code  
  #include <unistd.h>
#endif
This is used for I/O operations, file handling, and system calls.

Data Sections

2c initializes WebAssembly data sections in main():
int main(int argc, char** argv) {
  _memory = calloc(1, _memoryPages * 65536);
  
  // Initialize data sections
  memcpy(_memory + 1024, (unsigned char[]){72,101,108,108,111}, 5); // "Hello"
  _memory[2048] = (u8)42;
  _memory[2049] = (u8)0;
  
  // ... run program ...
  return 0;
}

Includes and Dependencies

2c automatically includes necessary headers:
#include <stdint.h>  // For i32, u8, etc.
#include <stdlib.h>  // For malloc, calloc
#include <string.h>  // For memcpy (if --2cMemcpy)
#include <math.h>    // For Math functions
#include <stdio.h>   // For I/O
The generated binary is typically self-contained with no runtime dependencies.

Performance Considerations

Optimization Tips

  1. Use Ofast: Default for maximum performance
    porf native script.js output --cO=Ofast
    
  2. Choose the Right Compiler:
    • Clang: Generally fastest, best optimization
    • GCC: Good alternative, widely available
    • Zig: Great for cross-compilation
  3. Profile Before Optimizing: Run with profiling first
    porf profile script.js
    
  4. Minimize Memory Allocations: Reuse objects in hot loops
  5. Use Typed Arrays: Better performance than regular arrays

Performance Comparison

Typical performance (relative to Node.js):
  • Wasm (interpreted): 2-5x faster
  • Wasm (JIT): Similar speed (after warmup)
  • Native (2c + Ofast): 3-10x faster
Performance varies greatly depending on the workload. I/O-heavy code may see less benefit than CPU-heavy code.

Cross-Compilation

Compile for different platforms using Zig:
# Compile for Linux from macOS
porf c script.js output.c
zig cc -target x86_64-linux -O3 output.c -o output-linux

# Compile for Windows from Linux
porf c script.js output.c
zig cc -target x86_64-windows -O3 output.c -o output.exe

# Compile for ARM from x86
porf c script.js output.c
zig cc -target aarch64-linux -O3 output.c -o output-arm

Debugging Native Binaries

Compile with Debug Info

# Disable stripping, use O0
porf c script.js output.c
clang -g -O0 output.c -o output-debug

Use GDB/LLDB

# GDB
gdb ./output-debug
(gdb) run
(gdb) backtrace

# LLDB  
lldb ./output-debug
(lldb) run
(lldb) bt

Common Issues

Segmentation Fault:
  • Usually memory access issues
  • Check pointer arithmetic in inline Wasm
  • Verify array bounds
Wrong Output:
  • Check type conversions
  • Verify memory layout assumptions
  • Compare against Wasm output
Compiler Errors:
  • Check generated C code for invalid constructs
  • Try different optimization levels
  • Report bugs to Porffor

Limitations

  1. No Sandboxing: Unlike Wasm, native code has full system access
  2. Platform-Specific: Binaries are platform-specific
  3. Experimental: May have bugs or generate incorrect code
  4. Limited Debugging: Harder to debug than JavaScript
  5. Compile Time: Slower than Wasm-only compilation

Security Considerations

Native binaries are NOT sandboxed. They have full access to:
  • File system
  • Network
  • System calls
  • Memory
Do not compile untrusted code to native binaries!
If you need sandboxing, use WebAssembly output instead:
porf wasm script.js output.wasm

Source Code

The 2c compiler is implemented in compiler/2c.js. Key components:
  • Type translation: Wasm types → C types
  • Instruction translation: Wasm opcodes → C code
  • Memory model: Linear memory allocation
  • Function generation: Wasm functions → C functions
  • Data initialization: Wasm data sections → C arrays

Examples

Hello World

// hello.js
console.log('Hello from native code!');
porf native hello.js hello
./hello
# Output: Hello from native code!

Fibonacci (Performance)

// fib.js
function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

console.log(fib(40));
porf native fib.js fib --cO=Ofast
time ./fib
# Much faster than Node.js!

File Processing

// process.js  
const fs = require('fs');
const data = fs.readFileSync('input.txt', 'utf8');
const result = data.toUpperCase();
console.log(result);
porf native process.js process
./process

Best Practices

  1. Test in Wasm First: Validate correctness before compiling to native
  2. Use Optimization: Always use -cO=Ofast or -cO=O3 for production
  3. Profile: Use porf profile to find bottlenecks
  4. Benchmark: Compare native vs. Wasm vs. Node.js
  5. Check Generated C: Review porf c output for issues
  6. Keep It Simple: Complex inline Wasm may not translate well

Future Improvements

Planned enhancements for 2c:
  • Better optimization passes
  • SIMD support translation
  • Smaller binary sizes
  • Faster compilation
  • Better debugging support
  • More platform-specific optimizations

Getting Help

If you encounter issues with 2c:
  1. Check generated C code: porf c script.js output.c
  2. Try different compilers: --compiler=gcc vs --compiler=clang
  3. Test with Wasm first: porf wasm script.js test.wasm
  4. Report bugs on GitHub
  5. Ask in Discord

Build docs developers (and LLMs) love