В D все очень плохо со встраиваемыми скриптовыми языками. Перепробовал много вариантов — Lua, Python, AngelScript — везде боль, все делается через неимоверно сложные API, где элементарно привязать к скрипту свою функцию — это целый квест. Чтобы, например, использовать биндинг dangel, нужно патчить рантайм языка для поддержки соглашения вызовов функций D, без этого там ничего не будет работать. Под Lua-биндинги очень сложно найти нужную версию библиотеки, чтобы приложение не крашилось. Pyd, привязка Python — вообще какой-то фантастически запутанный фреймворк из compile-time костылей. Нативные языки, написанные на D, по большей части устаревшие, неподдерживаемые и тупо не компилируются.

Поэтому я уже много лет назад начал пилить свой язык GScript. Изначально это был больше учебный проект, у меня все не хватало времени довести до ума виртуальную машину — первые два варианта GScript являются просто прототипами. Третья итерация, кажется, приобретает уже законченный вид.

Это будет, как обычно, JavaScript-подобный язык с динамической типизацией, компилируемый в байт-код. Я сильно упростил ISA и саму виртуальную машину, избавившись от лишних аллокаций, и теперь код выполняется намного быстрее — также он гораздо эффективнее сериализуется для сохранения в бинарный файл.

Вот как выглядит простейший «Hello, World»:

print "Hello, World!";

Синтаксис во многом аналогичен базовому JS (функции, объекты, массивы), но я ввел несколько важных отличий. Например, вариативные аргументы у меня реализованы при помощи специального оператора $, а список именованных аргументов необязателен.

func test
{
    print $0;
    print $1;
    print $2;
}

test(5, 10, 20);

Список всех аргументов вызова можно получить оператором $$. Он не создает новый массив, а использует срез буфера аргументов во фрейме, так что механизм очень эффективный:

func test
{
    const args = $$;
    print args.length;
    args[0] = 100;
    print $$; // первый аргумент теперь 100
}

test(5, 10, 20);

Объекты используют прототипное ООП, как в JS, и при вызове метода неявно передают ссылку на себя первым аргументом, как в Python. При наследовании от прототипа данные в новый объект не копируются, а просто вставляется «секретная» ссылка на прототип, которая используется для доступа к свойствам.

const Obj = {
    prop: "bar",
    test: func(self)
    {
        print self.prop;
    }
};

const f = new Obj;

Obj.prop = 10;   // свойство изменили в прототипе
f.test();        // выведет 10

f.prop = 5;
f.test();        // выведет 5 (переопределили в объекте-наследнике)

Пожалуй, самым необычным в языке является механизм модулей и импорта. Я намеренно не стал делать неймспейсы — все глобальные сущности программы живут в одном пространстве имен, как в C. Импорт осуществляется путем обращения к специальному объекту global, в который нужно сохранять все, что должно быть кросс-модульно. Фактически модули просто конструируют глобальные прототипы объектов, которые затем можно использовать откуда угодно (похожая идея, только с классами, реализована в Haxe).

foo.gs:

global.Foo = {
    prop: "Foo.prop",
    test: func(self)
    {
        print self.prop;
    }
};

main.gs:

import Foo from "foo.gs";

Foo.test();

Foo в данном случае — это просто автоматически созданная компилятором константа, хранящая global.Foo.

Такой подход означает, что в модулях лучше не объявлять глобальные переменные и функции, чтобы не создавать потенциальные конфликты имен.

Скомпилировать и выполнить скрипт очень просто, это буквально пять строк:

import gscript;

void main()
{
    string script = "print \"Hello, World!\";";
    GsInstruction[] bytecode = compile(script);

    auto vm = new GsVirtualMachine();
    vm.load(bytecode);
    vm.run();
}

Легко привязывать глобальные функции:

GsDynamic printSum(GsDynamic[] args)
{
    auto vm = cast(GsVirtualMachine)args[0].asObject;
    auto a = args[1].asNumber;
    auto b = args[2].asNumber;
    writeln(a + b);
    return GsDynamic(0);
}

vm["printSum"] = &printSum;

На стороне скрипта:

global.printSum(5, 3);

На текущей стадии уже можно привязывать целые классы — достаточно предоставить кастомную реализацию интерфейса GsObject:

class TestObj: GsObject
{
    int x = 0;

    this()
    {
    }

    GsDynamic foo(GsDynamic[] args)
    {
        writeln("TestObj.foo called");
        return GsDynamic(0);
    }

    GsDynamic get(string key)
    {
        if (key == "x")
            return GsDynamic(x);
        else if (key == "foo")
            return GsDynamic(&foo);
        else
            return GsDynamic(0.0);
    }

    void set(string key, GsDynamic value)
    {
        if (key == "x")
            x = cast(int)value.asNumber;
    }

    bool contains(string key)
    {
        if (key == "x") return true;
        else if (key == "foo") return true;
        else return false;
    }

    GsObject dup()
    {
        TestObj copy = new TestObj();
        copy.x = x;
        return copy;
    }
}

TestObj test = new TestObj();
vm["test"] = test;

На стороне скрипта:

global.test.x = 10;
print global.test.x;
global.test.foo();

Оставить комментарий

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