Наследуем тип .NET от JavaScript объекта с перегрузками и приватными методами

в 11:43, , рубрики: .net, javascript, не курю, ненормальное программирование

Да, именно так и никаких уловок. Эта идея мою голову посетила около двух месяцев назад в процессе обдумывания статьи об Алгоритмах и решениях. Типы .NET в том движке использовать легко, а можно ли наоборот…

Немного о движке

В предыдущей статье я старался меньше писать об особенностях и больше о том, что может быть полезно в отрыве от cкриптования вообще, дабы статья была меньше похожа на саморекламу. Буквально, с первых комментариев я понял, что это было ошибкой.
Скорость
Он быстр. Очень быстр. За прошедший месяц с небольшим было перелопачено много кода и разобрано множество частных случаев и сделаны выводы. К примеру, в зависимости от того, как написана функция и какие языковые средства используются, её вызов может происходить по одному из более чем 10 сценариев от эквивалента инлайна до честного выделения памяти под каждую переменную и аргумент и полную инициализацию контекста.
Простота интеграции
Вся тяжелая «кухня» по обеспечению доступа к типам платформы скрыта за одной скромной функцией
JSObject TypeProxy.Proxy(object)

Вы просто отдаёте практически любой объект и результат назначаете переменной

var script = new Script(" megaObject.alert('Hello from javascript') ");
script.Context.DefineVariable("megaObject")
              .Assign(TypeProxy.Proxy(new { alert = new Action<string>(x => MessageBox.Show(x)) });
script.Invoke();

Для типов, реализующих интерфейс IList есть специальная обёртка NiL.JS.Core.TypeProxing.NativeList, маскирующая такой объект под родной массив js.
Можно зарегистрировать конструктор типа и создавать объекты уже во время выполнения сценария. Добавится переменная с именем типа.

script.Context.AttachModule(typeof(System.Windows.Forms.Form));

Если лень добавлять типы по одному, можно добавить целое пространство имён

Context.GlobalContext.DefineVariable
                            ("forms") // имя переменной, через которую будет доступно пространство имён.
                            .Assign(new NamespaceProvider
                                ("System.Windows.Forms")); // пространство имён, к которому будет осуществляться доступ.

Не только выполнение
Каждый узел синтаксического дерева доступен извне сборки. Вы можете реализовать свою виртуальную машину, транслятор в другой язык, статический анализатор (если будет недостаточно интегрированного) или ещё что-то, на что способна ваша фантазия. Каждое использование всех переменных хранит ссылку на соответствующий дескриптор, который может рассказать некоторую информацию о ней. Например, показать все места использования, которые «выжили» после оптимизации. А пару недель назад был реализован, так называемый, Visitor с помощью которого всё перечисленное сделать ещё проще.

Общая схема

Для того, чтобы наследовать тип в платформе .NET нужна сборка, лежащая на диске, в которой хранится информация о типе (метаданные). Но пока JS файл лежит на диске, там никаких типов нет. Они там появятся только во время выполнения, когда логика этого сценария разделит функции на, собственно, функции и конструкторы. Решение этой загвоздки нашлось почти сразу — я добавил в глобальный контекст функцию, которая принимает на вход конструктор («registerClass»). Таким образом, я, как бы, прошу мне показать, для каких js-функций стоит генерировать метаданные. Однако, это требует холостого запуска.
После того, как выполнение закончилось, с помощью System.Reflection.Emit создаётся сохраняемая сборка в которой объявляется по одному классу для каждого зарегистрированного конструктора плюс ещё один статический, который будет хранить код javascript и запускать его при первом обращении. На этом этапе запуск registerClass() уже используется для того, чтобы поставить в соответствие типы в сборке с типами в сценарии. Все типы-обёртки унаследованы от одного типа в котором описаны базовые механизмы взаимодействия. Состав типов формируется на основе того, что было найдено в прототипе конструктора.

function ctor() {
}
ctor.prototype // есть у каждой функции кроме некоторых встроенных

Таким образом, если добавить неперечисляемое свойство в прототип, оно не попадёт в метаданные и станет «приватным».

Отлично, теперь можно создавать объекты JS как объекты .NET, но где обещанные наследование и перегрузки?!

Это решилось проще. Для того, чтобы понять, какие методы перегружены, в конструкторе базового типа выполняется проход по всем функциям в типе (понимаю, не совсем удачное место, но для прототипа реализации этого достаточно) и проверяется предок типа их объявившего. Все переопределённые методы становятся объявленными в типе-наследнике. Все методы, которые найдутся такой проверкой, добавляются в JS реализацию типа. Всё.
Теперь даже новые добавленные функции будут доступны в js, им не требуется быть перегруженными.

Код получился не большим, вы можете посмотреть его на GitHub

P.S. Это решение, по крайней мере, на данном этапе, не пригодно для серьёзного использования. Это эксперимент.

Автор: IaIojek

Источник

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


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