Есть такой язык программирования, который называется 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#, используя всю мощь языка и сопутствующих инструментов разработки.
Автор: DreamWalker