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

от автора

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

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

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

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

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

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

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

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

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

Код игры включает 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);     }; }); 

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

RayTracer

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

Отладка

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

На текущий момент отладка возможна в 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, но уже на сегодняшний день список фич приятно радует глаз:

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

ссылка на оригинал статьи http://habrahabr.ru/post/252079/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *