Once upon a time there was created a wonderful, general purpose programming language called C#. Many developers around the world embraced C#, and began writing all of their applications in C#, which ran on the user’s personal computers, and everyone was happy. Over the years, however, the landscape shifted - everything became more mobile, and there came a need to run some of those projects on the web. As it happened, the language of the web is JavaScript, and while it is ideal for writing applications that run in the browser, it’s not always possible (or viable) to rewrite significant parts of core business logic in JavaScript. There have been many attempts over the years to create languages that translate to JavaScript (TypeScript, CoffeeScript, to name a few), but none really provide the raw power and tooling, available to C# developers. Until now!

Enter DuoCode.

With DuoCode, our goal from the start was to create the best and most complete C# to JavaScript compiler, using the best technology available to date: the .NET Compiler Platform, formerly known as Microsoft “Roslyn”. Roslyn allows us to fully “understand” C# and all of its features (including the new C# 6 language features), and accurately convert them to corresponding JavaScript. With DuoCode, you use things like Generics, Reflection, LINQ and dynamic, while still enjoying full Visual Studio support for IntelliSense, syntax highlighting, and even debugging your original C# code in your browser!

Let’s start with our *Hello World” example. Given the following C# code:

// Original C# code
using System;
using DuoCode.Dom;
using static DuoCode.Dom.Global; // C# 6.0 'using static' syntax

namespace HelloDuoCode
{
  static class Program
  {
    public class Greeter
    {
      private readonly HTMLElement element;
      private readonly HTMLElement span;
      private int timerToken;

      public Greeter(HTMLElement el)
      {
        element = el;
        span = document.createElement("span");
        element.appendChild(span);
        Tick();
      }

      public void Start()
      {
        timerToken = window.setInterval((Action)Tick, 500);
      }

      public void Stop()
      {
        window.clearTimeout(timerToken);
      }

      private void Tick()
      {
        span.innerHTML = string.Format("The time is: {0}", DateTime.Now);
      }
    }

    static void Run()
    {
      System.Console.WriteLine("Hello DuoCode");

      var el = document.getElementById("content");
      var greeter = new Greeter(el);
      greeter.Start();
    }
  }
}

Compiling the above code will produce the following JavaScript:

// JavaScript code generated by DuoCode
var HelloDuoCode = this.HelloDuoCode || {};
var $d = DuoCode.Runtime;
HelloDuoCode.Program = $d.declare("HelloDuoCode.Program", System.Object, 0, $asm, function($t, $p) {
    $t.Run = function Program_Run() {
        System.Console.WriteLine$10("Hello DuoCode");

        var el = document.getElementById("content");
        var greeter = new HelloDuoCode.Program.Greeter.ctor(el);
        greeter.Start();
    };
});
HelloDuoCode.Program.Greeter = $d.declare("Greeter", System.Object, 0, HelloDuoCode.Program, function($t, $p) {
    $t.$ator = function() {
        this.element = null;
        this.span = null;
        this.timerToken = 0;
    };
    $t.ctor = function Greeter(el) {
        $t.$baseType.ctor.call(this);
        this.element = el;
        this.span = document.createElement("span");
        this.element.appendChild(this.span);
        this.Tick();
    };
    $t.ctor.prototype = $p;
    $p.Start = function Greeter_Start() {
        this.timerToken = window.setInterval($d.delegate(this.Tick, this), 500);
    };
    $p.Stop = function Greeter_Stop() {
        window.clearTimeout(this.timerToken);
    };
    $p.Tick = function Greeter_Tick() {
        this.span.innerHTML = String.Format("The time is: {0}", $d.array(System.Object, [System.DateTime().get_Now()]));
    };
});

And we’ll get something that looks like this:

This example shows several key features of DuoCode:

  • The new C# 6 using static syntax is supported
  • Original C# code will be visible/debuggable in the developer tools (after enabling source-maps integration)
  • Can work with DOM elements
  • Functioning timer

But this is just the beginning! Let’s dig deeper!

Tic-Tac-Toe

Included in the project templates is a small tic-tac-toe game, written entirely in C#!

The game code uses language features such as enums and indexers:

public enum Player
{
    None = 0,
    X = 1,
    O = -1
}

public sealed class Board
{
    public static Player Other(Player player)
    {
        return (Player)(-(int)player);
    }

    private readonly Player[] Squares;

    public readonly int Count;

    public Player this[int position]
    {
        get
        {
            return Squares[position];
        }
    }

    public Board() // empty board
    {
        Squares = new Player[9];
    }

    private Board(Board board, Player player, int position) :
      this()
    {
        Array.Copy(board.Squares, Squares, 9);
        Squares[position] = player;

        Count = board.Count + 1;
    }

    public bool Full { get { return Count == 9; } }

    public Board Move(Player player, int position)
    {
        if (position < 0 ||
            position >= 9 ||
            Squares[position] != Player.None)
        {
            throw new Exception("Illegal move");
        }

        return new Board(this, player, position);
    }

    public Player GetWinner()
    {
        if (Count < 5)
            return Player.None;

        Player result;
        bool winning =
          IsWinning(0, 1, 2, out result) ||
          IsWinning(3, 4, 5, out result) ||
          IsWinning(6, 7, 8, out result) ||
          IsWinning(0, 3, 6, out result) ||
          IsWinning(1, 4, 7, out result) ||
          IsWinning(2, 5, 8, out result) ||
          IsWinning(0, 4, 8, out result) ||
          IsWinning(2, 4, 6, out result);

        return result;
    }

    private bool IsWinning(int p0, int p1, int p2, out Player player)
    {
        int count = (int)Squares[p0] + (int)Squares[p1] + (int)Squares[p2];
        player = count == 3 ? Player.X : count == -3 ? Player.O : Player.None;
        return player != Player.None;
    }
}

And we’re rendering DOM elements from the program’s Main() method:

public static void Main(string[] args)
{
  for (var i = 0; i < 9; i++)
  {
    Dom.HTMLInputElement checkbox = GetCheckbox(i);
    checkbox.checked_ = false;
    checkbox.indeterminate = true;
    checkbox.disabled = false;
    checkbox.onclick = OnClick;
  }

  if (new Random().Next(2) == 0)
    ComputerPlay();

  UpdateStatus();
}

private static dynamic OnClick(Dom.MouseEvent e)
{
  int position = int.Parse(((Dom.HTMLInputElement)e.target).id[1].ToString());

  try
  {
    board = board.Move(Player.X, position);
  }
  catch
  {
    Dom.Global.window.alert("Illegal move");
    return null;
  }

  Dom.HTMLInputElement checkbox = GetCheckbox(position);
  checkbox.disabled = true;
  checkbox.checked_ = true;

  if (!board.Full)
    ComputerPlay();

  UpdateStatus();

  return null;
}

private static Dom.HTMLInputElement GetCheckbox(int index)
{
  string name = "a" + index.ToString();
  Dom.HTMLInputElement checkbox = Dom.Global.document.getElementById(name).As<Dom.HTMLInputElement>();
  return checkbox;
}

WebGL

Want to write WebGL apps? No problem! Let’s take the following C# code:

using DuoCode.Dom;
using System;

namespace WebGL
{
  using GL = WebGLRenderingContext;

  internal static class Utils
  {
    public static WebGLRenderingContext CreateWebGL(HTMLCanvasElement canvas)
    {
      WebGLRenderingContext result = null;
      string[] names = { "webgl", "experimental-webgl", "webkit-3d", "moz-webgl" };
      foreach (string name in names)
      {
        try
        {
          result = canvas.getContext(name);
        }
        catch { }
        if (result != null)
          break;
      }
      return result;
    }

    public static WebGLShader CreateShaderFromScriptElement(WebGLRenderingContext gl, string scriptId)
    {
      var shaderScript = (HTMLScriptElement)Global.document.getElementById(scriptId);

      if (shaderScript == null)
        throw new Exception("unknown script element " + scriptId);

      string shaderSource = shaderScript.text;

      // Now figure out what type of shader script we have, based on its MIME type
      int shaderType = (shaderScript.type == "x-shader/x-fragment") ? GL.FRAGMENT_SHADER :
                       (shaderScript.type == "x-shader/x-vertex")   ? GL.VERTEX_SHADER   : 0;
      if (shaderType == 0)
        throw new Exception("unknown shader type");

      WebGLShader shader = gl.createShader(shaderType);
      gl.shaderSource(shader, shaderSource);

      // Compile the shader program
      gl.compileShader(shader);

      // See if it compiled successfully
      if (!gl.getShaderParameter(shader, GL.COMPILE_STATUS))
      {
        // Something went wrong during compilation; get the error
        var errorInfo = gl.getShaderInfoLog(shader);
        gl.deleteShader(shader);
        throw new Exception("error compiling shader '" + shader + "': " + errorInfo);
      }
      return shader;
    }

    public static WebGLProgram CreateShaderProgram(WebGLRenderingContext gl, WebGLShader fragmentShader, WebGLShader vertexShader)
    {
      var shaderProgram = gl.createProgram();
      gl.attachShader(shaderProgram, vertexShader);
      gl.attachShader(shaderProgram, fragmentShader);
      gl.linkProgram(shaderProgram);

      bool linkStatus = gl.getProgramParameter(shaderProgram, GL.LINK_STATUS);
      if (!linkStatus)
        throw new Exception("failed to link shader");
      return shaderProgram;
    }

    public static WebGLTexture LoadTexture(WebGLRenderingContext gl, string resourceName)
    {
      var result = gl.createTexture();
      var imageElement = Properties.Resources.duocode.Image;
      imageElement.onload = new Func<Event, dynamic>((e) =>
      {
        UploadTexture(gl, result, imageElement);
        return true;
      });

      return result;
    }

    public static void UploadTexture(WebGLRenderingContext gl, WebGLTexture texture, HTMLImageElement imageElement)
    {
      gl.pixelStorei(GL.UNPACK_FLIP_Y_WEBGL, GL.ONE);
      gl.bindTexture(GL.TEXTURE_2D, texture);
      gl.texImage2D(GL.TEXTURE_2D, 0, GL.RGBA, GL.RGBA, GL.UNSIGNED_BYTE, imageElement);
      gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MAG_FILTER, GL.LINEAR);
      gl.texParameteri(GL.TEXTURE_2D, GL.TEXTURE_MIN_FILTER, GL.LINEAR_MIPMAP_NEAREST);
      gl.generateMipmap(GL.TEXTURE_2D);
      gl.bindTexture(GL.TEXTURE_2D, null);
    }

    public static float DegToRad(float degrees)
    {
      return (float)(degrees * System.Math.PI / 180);
    }
  }
}

Apply DuoCode magic to it, and you’ll get a WebGL app that runs in your browser!

Debugging

The best part of compiling with DuoCode is the ability to debug your code straight from the browser! We will automatically generate source maps, which allow mapping between the original C# code and the generated JavaScript, allowing you to set breakpoints, inspect variables and step through your code without leaving your browser’s development tools:

Debugging is currently supported only in Visual Studio 2015, using IE, Chrome and Firefox.

Few more examples

Here are a few more examples of DuoCode’s abilities:

Structs

C#:

public struct Point
{
    public readonly static Point Zero = new Point(0, 0);

    public readonly int X;
    public readonly int Y;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

JavaScript:

HelloDuoCode.Program.Point = $d.declare("Point", null, 62, HelloDuoCode.Program, function($t, $p) {
    $t.cctor = function() {
        $t.Zero = new HelloDuoCode.Program.Point.ctor$1(0, 0);
    };
    $t.ctor = function Point() {
        this.X = 0;
        this.Y = 0;
    };
    $t.ctor.prototype = $p;
    $t.ctor$1 = function Point(x, y) {
        this.X = x;
        this.Y = y;
    };
    $t.ctor$1.prototype = $p;
});

Note: only immutable structs are currently supported.

LINQ

C#:

public static IEnumerable<int> Foo()
{
    return Enumerable.Range(0, 10).Where(x => x % 2 == 0).Select(x => x * 3);
}

JavaScript:

$t.Foo = function Program_Foo() {
    return System.Linq.Enumerable.Select(System.Int32, System.Int32, System.Linq.Enumerable.Where(System.Int32,
        System.Linq.Enumerable.Range(0, 10), $d.delegate(function(x) {
            return x % 2 == 0;
        }, this)), $d.delegate(function(x) {
        return x * 3;
    }, this));
};

Miscellaneous

Generics, params arrays, nullable, method overloading, default values, all supported in DuoCode:

C#:

public class Foo<T> where T : IComparable<T>
{
    public void Bar(int? x, T y, string z = "value")
    {
        System.Console.WriteLine((x ?? -1) + y.ToString() + z);
    }
    public void Bar(string z, params object[] args)
    {
    }
}
// Main
new Foo<int>().Bar(null, 2);

JavaScript:

HelloDuoCode.Program.Foo$1 = $d.declare("Foo`1", System.Object, 256, HelloDuoCode.Program, function($t, $p, T) {
    $t.ctor = function Foo$1() {
        $t.$baseType.ctor.call(this);
    };
    $t.ctor.prototype = $p;
    $p.Bar$1 = function Foo$1_Bar(x, y, z) {
        System.Console.WriteLine$10($d.toString(($d.ncl(x, -1))) + y.ToString() + z);
    };
    $p.Bar = function Foo$1_Bar(z, args) {};
}, [$d.declareTP("T")]);
// Main
new (HelloDuoCode.Program.Foo$1(System.Int32).ctor)().Bar$1(null, 2, "value");

… and we’re just getting started!

Final words

DuoCode is currently in a closed beta. If you want to play with DuoCode, please sign up for an invite on our site. We’re looking forward to release a public beta version very soon!

Please let us know of any issues you might have, as well as suggestions on what you’d like to see in DuoCode! Believe us, wonderful things are coming!

Until then,

Happy coding!
– The DuoCode Team!


This post was adapted, with permission, from an original article in Russian by Andrey Akinshin. The original article is posted here: http://habrahabr.ru/company/enterra/blog/252079/



blog comments powered by Disqus

Contact Us

We’d love to hear from you