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
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
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
public class Foo
Разработка идёт достаточно быстро, постоянно выходят обновления с новыми возможностями. Будем надеяться, что мы уже буквально в паре шагов от того светлого будущего, когда можно будет писать действительно сложные клиентские веб-приложения на C#, используя всю мощь языка и сопутствующих инструментов разработки.