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#. Давайте посмотрим внимательнее, что же представляет из себя продукт.

cf4b40d9ed2c41ad93667444b809ecfb.png

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 }; }); Выглядит это примерно так: 0f85a64345374ceeafa520fe5d0d634f.png

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

Поддерживается синтаксис using static из C# 6.0. Можно легко работать с консолью, которая отображается внизу вашего приложения. Можно работать с DOM-элементами Работает таймер Даже этот простой пример уже радует. Но подобное приложение и на самом JavaScript не так сложно написать. Давайте посмотрим примеры поинтереснее.Крестики-нолики В дистрибутив входит пример написания замечательной HTML-игры, написанной на чистом C#: ba7c0be13b3940aeb3926fb1d7e8c86f.png

Код игры включает 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(); 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((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); }; }); Вы можете самостоятельно потыкать демку на официальном сайте. Выглядит это примерно так: 2788bd179edc439aa810cd25bedfa4ff.png

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

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

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

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

© Habrahabr.ru