BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles WebAssembly and Blazor: A Decades Old Problem Solved

WebAssembly and Blazor: A Decades Old Problem Solved

This item in japanese

Key Takeaways

  • WebAssembly is a new client-side technology that enables near-native performance without plug-ins in all modern browsers, including mobile.
  • Many languages, including C, C#, Go, and Rust, can compile code that targets the stack-based WebAssembly virtual machine.
  • WebAssembly makes running .NET code literally anywhere, including inside the browser, possible.
  • Blazor is a client-side library that uses .NET on WebAssembly to support Single Page Applications written in C# using Razor templates.
  • Blazor enables code reuse and the ability to port legacy code to modern web applications.

In mid-April 2019, Microsoft gently nudged a young framework from the "anything is possible" experimental phase to a "we're committed to making this happen" preview. The framework, named Blazor because it runs in the browser and leverages a templating system or "view engine" called Razor, enables the scenario .NET developers almost gave up on. It doesn't just allow developers to build client-side code with C# (no JavaScript required), but also allows developers to run existing .NET Standard DLLs in the browser without a plugin.

There are two Blazor-branded hosting models. This article focuses on the client-side version. You can learn more about the server-side version by reading: Blazor server-side hosting model.

The Silverlight Hope

The dream of running .NET anywhere began in 2006 with an application framework codenamed "Windows Presentation Foundation/Everywhere (WPF/E)" that was released to the public as Silverlight. The first version supported the declarative user interface introduced to the world via WPF called Extensible Application Markup Language, or XAML. The platform offered fine-grained control over UI elements and surfaced its own Document Object Model (DOM) that was accessible from JavaScript.

Adoption accelerated when Silverlight 2 was released in 2008 with full support for .NET via a Common Language Runtime (CLR) implementation that ran as a browser plug-in. Developers could use any .NET language to build their web apps, leverage mature data-binding patterns like Model-View-ViewModel (MVVM) and communicate with web APIs using REST or a Windows Communication Foundation (WCF) client. It was beginning to look like .NET developers could shake the JavaScript dust off their feet, stop worrying about cross-browser testing, and focus on one platform with a common code base to deliver their apps.

Unbeknownst to Silverlight developers, 2007 was a tough year for the platform. Two seemingly unrelated events transpired that would ultimately lead to its demise. First, a collaboration between the Web Hypertext Application Technology Working Group (WHATWG) and the World Wide Web Consortium (W3C) began work on the first draft of the HTML5 specification that would be published in 2008.

Second, on June 29, 2007, Apple released the iPhone.

Every once in a while, a revolutionary product comes along that changes everything.  Steve Jobs

The race was on. Cell phones evolved almost overnight from flip phones with contact lists to portable computers with games and built-in web browsers. For a short period, the future of Silverlight seemed promising. Microsoft's response to the iPhone, the Windows Phone 7, supported a flavor of Silverlight as the development platform. Chrome support was coming. If Microsoft could work out a way to get Silverlight onto iPhones and Android phones, the holy grail of "write once, run everywhere" would finally be discovered.

Only, it wasn't.

For many reasons, including the security concerns of running what, for practical purposes, was be a "virtual machine in the browser," and the potential battery drain, the door to browser plug-ins, specifically on mobile devices, slammed shut. The industry began to look to the promise of HTML5 for building mobile experiences. Microsoft shifted its own focus, and by the time Silverlight 5 was released in 2011, most developers saw the writing on the wall: there would be no new versions.

HTML5 and JavaScript continued to win the hearts and minds of web developers. Tools like jQuery normalized the DOM and made it easier to build multi-browser applications, while at the same time browser engines started to adopt a common DOM standard to make it easier to build once and run everywhere. An explosion of front-end frameworks like Angular, React, and Vue.js brought Single Page Applications (SPA) mainstream and cemented JavaScript as the language of choice for the browser operating system.

JavaScript as a Platform

In March of 2013, asm.js was introduced to the world. The documentation describes it as a strict subset of JavaScript that can be used as a low-level, efficient target language for compilers. The specification essentially defines a set of JavaScript conventions that make it possible to optimize code with ahead-of-time compilation and provides strict-typing (JavaScript itself is a dynamic language) and a heap-based memory model.

The introduction of asm.js opened a new realm of possibility by making it possible to compile C/C++ code to JavaScript. The restrictions on conventions enabled "asm.js-aware" engines to efficiently compile JavaScript to high performing native code. To better understand how this is possible, consider the following C snippet of code:

int find(char *buf, char test) {
    char *cur = buf;
    while (*cur != 0 && *cur != test) {
        cur++;
    } 
    if (*cur == 0) {
        return -1;
    }
    return (cur - buf);
}

The code effectively scans a string for a test character or zero-byte marking its end and computes the offset. C++ can already be compiled using a tool named Clang to byte code compliant with the LLVM tool chain. LLVM is a set of technologies that enable fast cross-platform compilation of code. A project named Emscripten leverages the tool chain to generate asm.js.

Compiling the C++ code with Emscripten generates dozens of lines of highly optimized JavaScript. The following code has been simplified to illustrate what was generated:

function find(buf, test) {
    buf = buf|0;
    var cur = buf|0;
    var result = -1|0;
    while (1) {
        var check = HEAP8[cur>>0]|0;
        var foundZero = (check) === (0);
        if (foundZero) {
            break;
        }
        var foundTest = (check) === (test|0);
        if (foundTest) {
            result = (cur - buf)|0;
            break;
        }
    }
    return result|0;
}

The generated JavaScript is compatible with and runs fine in all browsers. The exclusive-or with zero operation (|0) simply turns any number into a signed integer. In older browsers, this ensures the number has no fractional component. In modern browsers, the convention informs the ahead-of-time compiler to use 32-bit integers (resulting in faster math operations) rather than the default 64-bit floating point values. The right-shift of zero (>>0) prevents overflow and also declares an "index" integer type that iterates over HEAP8, a typed buffer of bytes made available to asm.js.

There is no for loop defined in asm.js. Everything is translated to a while(1) loop. This makes it easier to apply compiler optimizations. The optimizations are so effective that a team was able to port the Unreal 4 engine to run 3D first person games directly in the web browser with near-native performance.

WebAssembly: A New Hope

Fast forward to 2017 and the release of WebAssembly, a binary instruction format for a stack-based virtual machine. WebAssembly provides a portable compile target (called Wasm, for short) that has several advantages over asm.js:

  • As a byte code format, there is no need to parse script and pre-compile for optimization. The code can be directly translated to native instructions. Startup times to load and begin execution of the code are orders of magnitude faster compared to asm.js.
  • The byte code format is a more compact way to deliver code.
  • Wasm implements its own instruction set and is therefore not constrained by the JavaScript language.

Any code that compiles to asm.js can target WebAssembly. With the previous example, a simple change in a compiler flag will generate a file with the .wasm extension. The file is only 116 bytes in length. Although the file contains byte code, a standardized text representation of the code exists named the WebAssembly text format. This is the text representation of the find module in WebAssembly:

(module
  (type $t0 (func (param i32 i32) (result i32)))
  (import "env" "memory" (memory $env.memory 256 256))
  (func $a (type $t0) (param $p0 i32) (param $p1 i32) (result i32)
    (local $l0 i32) (local $l1 i32) (local $l2 i32) (local $l3 i32)
    get_local $p0
    set_local $l0
    loop $L0
      get_local $l0
      i32.load8_s
      tee_local $l2
      i32.eqz
      set_local $l1
      get_local $l0
      i32.const 1
      i32.add
      set_local $l3
      get_local $l1
      i32.const 1
      i32.xor
      get_local $p1
      i32.const 24
      i32.shl
      i32.const 24
      i32.shr_s
      get_local $l2
      i32.ne
      i32.and
      if $I1
        get_local $l3
        set_local $l0
        br $L0
      end
    end
    i32.const -1
    get_local $l0
    get_local $p0
    i32.sub
    get_local $l1
    select)

The code has been optimized for size, so the function was renamed to a.

WebAssembly is now in a stable 1.x release and supported by all modern browsers, including mobile. Several languages have adopted Wasm as a valid compilation target. You can build WebAssembly programs using C, C++, Go, Rust, TypeScript, and dozens of other languages. It has been implemented in solutions for computer vision, audio mixing, video codec support, digital signal processing, medical imaging, physical simulations, encryption, compression, and more.

But what about C#?

Immediately after WebAssembly was introduced, work began to port a working version of the .NET Framework (including its Common Language Runtime) to run on WebAssembly.

The effort was successful.

The Browser and Razor View Engine

Microsoft software engineer Steve Sanderson announced Blazor on his personal blog in late 2017. At the time it was "just an experiment" and not an official product. It began with the question, "How can we get .NET to run in WebAssembly?" The first answer was an older, compact version of the .NET runtime that he was able to compile as a Wasm binary in just a few hours. .NET by itself isn't incredibly useful in the browser: you need a UI and some way to interact with the user. Building on the stable work of Razor files that combine markup and C# to create web-templates, Blazor added a slew of services from data-binding and dependency injection to reusable components, layouts, and the ability to call to and from JavaScript. All these services combine to make it possible to build Single Page Applications (SPA) using .NET and C#.

Figure 1: Default Blazor application

Why should anyone care? Initial developer reactions to Blazor were overwhelmingly positive. Here are a few reasons why:

  • It allows developers to use a language (C#) and framework (.NET) they are already familiar with to build client-side apps that were formerly entrenched in JavaScript.
  • It runs in all modern browsers, including mobile browsers, without plug-ins.
  • It empowers developers to tap into the .NET ecosystem and use existing libraries "as is." For example, if you are building a blog engine that uses markdown, you can install the NuGet package for an existing markdown engine and convert markdown to HTML for preview directly in the browser.
  • .NET performance has continued to improve over time and therefore is more than adequate to run on top of Wasm in the browser.
  • Blazor is a true Single Page Application that runs from a set of static assets that can be hosted at a very low cost using services like Azure Storage static websites.

Now that you know the history and motivation behind Blazor, let's explore some of the technical details.

All code samples from this article are available in the Blazor WebAssembly GitHub repository.

Everything you need to install Blazor and get started is available in the get started with Blazor article. After Blazor is installed, you can choose to create a client-only or a client with ASP.NET Core backend project. The project looks strikingly familiar to existing server-side MVC-based projects. The DLLs generated, however, are loaded directly into the browser and run by the WebAssembly version of .NET.

Figure 2: Network activity in a Blazor app

The mono.js JavaScript dynamically loads mono.wasm and begins running .NET in the browser. The remaining loads are the actual DLL files that make up the application.

C# in the Browser (with Dependency Injection)

The default template includes a page to fetch mock weather information. This is the Razor view that is rendered entirely by Wasm on the client.

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

At the top of the template, a set of directives determine the route of the page, declare a using directive and use dependency injection to get a copy of the .NET Framework's HttpClient that is ready to use inside the browser.

@page "/fetchdata"
@using GetStarted.Shared
@inject HttpClient Http

Finally, a bit of code on the page is embedded in a @functions block. What's important to note here is that the code is entirely C#. Network operations can be performed with the familiar HttpClient and async/await are supported.

WeatherForecast[] forecasts;

protected override async Task OnInitAsync()
{
    forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts");
}

The view template renders a page, but what about controls?

Reusable Components

Blazor is based on a composable UI of hierarchical components. The only thing that distinguishes the weather forecast component from any other is the page directive that provides a route. Here is the template for a component named LabelSlider.razor that augments the built-in HTML input range with a span that shows the current value.

<input type="range" min="@Min" max="@Max" bind-value-oninput="@CurrentValue" />
<span>@CurrentValue</span>

The binding syntax is in the format bind-{property}-{event}. The event is optional and updates the binding anytime the event is fired. Without this, the slider would only update the span when the user stops moving the slider bar. By hooking into oninput the value is refreshed as the slider moves.

The associated code exposes parameters that allow parent components to set the minimum and maximum range values and data-bind to the current value. The Action property exposes an event associated with CurrentValue and is named, by convention, CurrentValueChanged to facilitate two-way data-binding (parent components can "listen" to change events and update bound values accordingly).

[Parameter]
int Min { get; set; }

[Parameter]
int Max { get; set; }

private int _currentValue;

[Parameter]
int CurrentValue
{
    get => _currentValue;
    set
    {
        if (value != _currentValue)
        {
            _currentValue = value;
            CurrentValueChanged?.Invoke(value);
        }
    }
}

[Parameter]
Action<int> CurrentValueChanged { get; set; }

Note that properties not tagged with the Parameter attribute are only visible to the component. Reusing the component is as simple as dropping in a tag with the component name and supplying the necessary parameters. Here it is in use:

<LabelSlider Min="0" Max="99" bind-CurrentValue="@currentCount"/>

In this example, two-way binding is established with the currentCount property on the parent component.

Use Existing Libraries

A very powerful benefit of Blazor is the ability to integrate existing class libraries "as is." For example, consider a blog engine that uses markdown with the ability to preview the generated HTML in the browser. Building this in Blazor is as simple as installing a NuGet package, in this case the open source MarkDig processor. The library can then be invoked directly:

var html = Markdig.Markdown.ToHtml(SourceText);

The NuGet DLL is imported into the browser like any other project references and available to call from the client-side app.

Figure 3: Markdown converted in the browser

Call To and From JavaScript

An important service that Blazor provides is the ability to call JavaScript from .NET and vice versa. Any JavaScript methods you wish to call from Blazor must be accessible from the global window object. To call this:

window.jsAlert = msg => alert(msg);

The interop functionality is used like this:

await JsRuntime.InvokeAsync<object>("jsAlert", "Wow!");

The InvokeAsync method supports passing and returning values, and the values will be automatically converted to/from JavaScript/.NET and marshalled by the Blazor runtime. Use the JsInvokable attribute to expose a C# method so it can be called from JavaScript. Here is an example that wraps the markdown conversion call:

public static class Markdown
{
    [JSInvokable]
    public static string Convert(string src)
    {
        return Markdig.Markdown.ToHtml(src);
    }
}

From JavaScript, call DotNet.invokeMethod and pass the assembly name, the exposed method name, and any parameters.

Figure 4: Calling .NET from JavaScript

This makes it possible to extend legacy applications and use existing JavaScript libraries. You can even call into other WebAssembly modules from your Blazor app.

Blazor is Moving Forward

Microsoft moved Blazor out of the experimental phase and into official preview. A version of Blazor that uses the component model for server-side rendering will ship with the final release of .NET Core 3 (see the .NET Core roadmap), and the client release will follow soon after. There is work left to finish. The debug experience is extremely limited and must be improved; there is opportunity to optimize the code performance by generated native Wasm with ahead-of-time compilation; the overall size needs to be reduced by shaving unused code out of libraries before shipping them to the browser (a process known as tree-shaking). Interest and adoption of WebAsssembly is growing every day, and with Blazor the dream of writing C# and .NET code that can literally run anywhere is finally realized.

About the Author

Jeremy Likness is a Cloud Advocate for Azure at Microsoft. Jeremy wrote his first program in 1982 and has been developing enterprise applications for 25 years. He is the author of four technology books, a former 8-year Microsoft MVP, and an international and keynote speaker. Jeremy follows a plant-based diet and spends most of his free time running, hiking and camping near his home in the Pacific Northwest. Follow Jeremy on his blog.

Rate this Article

Adoption
Style

BT