DuoCode: транслируем C# в JavaScript

в 4:25, , рубрики: .net, C#, duocode, javascript, WebGL, Блог компании Enterra, Веб-разработка

Есть такой язык программирования, который называется C#. И есть очень много разработчиков, которым он очень нравится. А ещё есть такой язык программирования, который называется JavaScript. Как-то так сложилось, что он нравится далеко не всем C#-разработчикам. А теперь представьте ситуацию: есть заядлый C#-разработчик. Он очень любит C#, все-все проекты на нём пишет. Но судьба распорядилась так, что ему понадобилось написать клиентское веб-приложение. Знаете, такое, чтобы пользователю не нужно было себе ничего скачивать и устанавливать, чтобы он мог просто открыть любой браузер в любой операционной системе на любом устройстве — а приложение уже там. И вот тут у нашего лирического героя возникла проблема: вроде бы JavaScript идеально подходит для этой задачи, но вот писать на нём отчего-то на нём не очень хочется. К счастью, в современном мире существует много языков, которые транслируются в JavaScript (всякие TypeScript, CoffeScript и тысячи других). Но наш разработчик оказался очень упрямым: он упорно не хочет изменять своему любимому C# с «вражескими» технологиями.

К счастью для него, счастливое будущее уже практически наступило. Есть такой проект, который называется DuoCode. Он умеет транслировать C#-код в JavaScript. Пока он в состоянии beta, но у него уже весьма неплохо получается: поддерживаются нововведения C# 6.0, Generic-типы, Reflection, структуры и LINQ, а отлаживать итоговый JavaScript можно на исходном C#. Давайте посмотрим внимательнее, что же представляет из себя продукт.

DuoCode: транслируем C# в JavaScript - 1

Hello DuoCode

Понять происходящее проще всего на примерах. Начнём с классического *Hello world*. Итак, имеем замечательный C#-код:

// 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();
    }
  }
}

Лёгким движением руки он превращается в 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()])); // try to put a breakpoint here
    };
});

Выглядит это примерно так:

DuoCode: транслируем C# в JavaScript - 2

Подскажу, на что стоит обратить внимание:

  • Поддерживается синтаксис using static из C# 6.0.
  • Можно легко работать с консолью, которая отображается внизу вашего приложения.
  • Можно работать с DOM-элементами
  • Работает таймер

Даже этот простой пример уже радует. Но подобное приложение и на самом JavaScript не так сложно написать. Давайте посмотрим примеры поинтереснее.

Крестики-нолики

В дистрибутив входит пример написания замечательной HTML-игры, написанной на чистом C#:

DuoCode: транслируем C# в JavaScript - 3

Код игры включает enum-ы и индексаторы:

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];
        Squares = new Player[] { Player.None, Player.None, Player.None, Player.None, Player.None, Player.None, Player.None, Player.None, Player.None };
    }

    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;
    }
}

Обратите внимание, как ловно удаётся управляться с DOM-элементами:

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

Хотите работать с WebGL? Нет проблем! Берём C#-код:

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);
    }
  }
}

И применяем к нему DuoCode-магию:

WebGL.Utils = $d.declare("WebGL.Utils", System.Object, 0, $asm, function($t, $p) {
    $t.CreateWebGL = function Utils_CreateWebGL(canvas) {
        var result = null;
        var names = $d.array(String, ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"]);
        for (var $i = 0, $length = names.length; $i != $length; $i++) {
            var name = names[$i];
            try {
                result = canvas.getContext(name);
            }
            catch ($e) {}

            if (result != null)
                break;
        }
        return result;
    };
    $t.CreateShaderFromScriptElement = function Utils_CreateShaderFromScriptElement(gl, scriptId) {
        var shaderScript = $d.cast(document.getElementById(scriptId), HTMLScriptElement);

        if (shaderScript == null)
            throw new System.Exception.ctor$1("unknown script element " + scriptId);

        var shaderSource = shaderScript.text;

        // Now figure out what type of shader script we have, based on its MIME type
        var shaderType = (shaderScript.type == "x-shader/x-fragment") ? 35632 /* WebGLRenderingContext.FRAGMENT_SHADER */ : (shaderScript.type == "x-shader/x-vertex") ? 35633 /* WebGLRenderingContext.VERTEX_SHADER */ : 0;
        if (shaderType == 0)
            throw new System.Exception.ctor$1("unknown shader type");

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

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

        // See if it compiled successfully
        if (!gl.getShaderParameter(shader, 35713 /* WebGLRenderingContext.COMPILE_STATUS */)) {
            // Something went wrong during compilation; get the error
            var errorInfo = gl.getShaderInfoLog(shader);
            gl.deleteShader(shader);
            throw new System.Exception.ctor$1("error compiling shader '" + $d.toString(shader) + "': " + errorInfo);
        }
        return shader;
    };
    $t.CreateShaderProgram = function Utils_CreateShaderProgram(gl, fragmentShader, vertexShader) {
        var shaderProgram = gl.createProgram();
        gl.attachShader(shaderProgram, vertexShader);
        gl.attachShader(shaderProgram, fragmentShader);
        gl.linkProgram(shaderProgram);

        var linkStatus = gl.getProgramParameter(shaderProgram, 35714 /* WebGLRenderingContext.LINK_STATUS */);
        if (!linkStatus)
            throw new System.Exception.ctor$1("failed to link shader");
        return shaderProgram;
    };
    $t.LoadTexture = function Utils_LoadTexture(gl, resourceName) {
        var result = gl.createTexture();
        var imageElement = WebGL.Properties.Resources().get_duocode().Image;
        imageElement.onload = $d.delegate(function(e) {
            WebGL.Utils.UploadTexture(gl, result, imageElement);
            return true;
        }, this);

        return result;
    };
    $t.UploadTexture = function Utils_UploadTexture(gl, texture, imageElement) {
        gl.pixelStorei(37440 /* WebGLRenderingContext.UNPACK_FLIP_Y_WEBGL */, 1 /* WebGLRenderingContext.ONE */);
        gl.bindTexture(3553 /* WebGLRenderingContext.TEXTURE_2D */, texture);
        gl.texImage2D(3553 /* WebGLRenderingContext.TEXTURE_2D */, 0, 6408 /* WebGLRenderingContext.RGBA */, 6408 /* WebGLRenderingContext.RGBA */, 5121 /* WebGLRenderingContext.UNSIGNED_BYTE */, imageElement);
        gl.texParameteri(3553 /* WebGLRenderingContext.TEXTURE_2D */, 10240 /* WebGLRenderingContext.TEXTURE_MAG_FILTER */, 9729 /* WebGLRenderingContext.LINEAR */);
        gl.texParameteri(3553 /* WebGLRenderingContext.TEXTURE_2D */, 10241 /* WebGLRenderingContext.TEXTURE_MIN_FILTER */, 9985 /* WebGLRenderingContext.LINEAR_MIPMAP_NEAREST */);
        gl.generateMipmap(3553 /* WebGLRenderingContext.TEXTURE_2D */);
        gl.bindTexture(3553 /* WebGLRenderingContext.TEXTURE_2D */, null);
    };
    $t.DegToRad = function Utils_DegToRad(degrees) {
        return (degrees * 3.14159265358979 /* Math.PI */ / 180);
    };
});

Вы можете самостоятельно потыкать демку на официальном сайте. Выглядит это примерно так:

DuoCode: транслируем C# в JavaScript - 4

RayTracer

И это не предел! Один из примеров включает полноценный RayTracer (с векторной математикой, работой с цветом и освещением, камерой и поверхностями — всё на чистом C#):

DuoCode: транслируем C# в JavaScript - 5

Отладка

Звучит невероятно, но отлаживать это чудо можно прямо в браузере. C#-исходники прилагаются:

DuoCode: транслируем C# в JavaScript - 6

На текущий момент отладка возможна в VS 2015, IE, Chrome и Firefox.

Ещё пара примеров

При трансляции из C# в JavaScript одним из самых больных вопросов являются структуры. Сегодня DuoCode поддерживает только неизменяемые структуры, но для хорошего проекта этого должно хватить (как мы знаем, мутабельных структур следует избегать).

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;
});

Лично меня особенно радует, что есть полноценная поддержка 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));
};

Мелкие радости вроде Generic, params, nullable, перегрузка методов, значения по умолчанию также идут в комплекте:

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");

Заключение

Напомню, что DuoCode пока находится в состоянии beta, но уже на сегодняшний день список фич приятно радует глаз:

DuoCode: транслируем C# в JavaScript - 7

Разработка идёт достаточно быстро, постоянно выходят обновления с новыми возможностями. Будем надеяться, что мы уже буквально в паре шагов от того светлого будущего, когда можно будет писать действительно сложные клиентские веб-приложения на C#, используя всю мощь языка и сопутствующих инструментов разработки.

Автор: DreamWalker

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js