ObjectScript API, интеграция с C++. Часть 4: подключение пользовательских классов и функций на C++

в 13:57, , рубрики: api, binding, c++, integration, javascript, Lua, ObjectScript, objectscript api, open source, php, Программирование, метки: , , , , , , , ,

ObjectScript — новый встраиваемый объектно-ориентированный язык программирования с открытым исходным кодом. ObjectScript расширяет возможности таких языков, как JavaScript, Lua и PHP.

По результатам предыдущих статей было много вопросов о том, как подключать свои классы и функции на C++ к ObjectScript. Имеющийся в первых сборках OS способ подключения едва ли удовлетворял потребности и я решил сделать более мощный и удобный биндинг, который теперь идет в комплекте с OS поумолчанию.

В чем собственно преимущество нового биндинга: теперь можно подключать любую функцию, с любыми параметрами, любым возвращаемым значением без дополнительных оберток. Сразу подключаете ту функцию, которая у вас есть и все, готово. И будьте уверены, что при вызове C++ функции из скрипта на OS она получит правильные параметры, а возвращаемое из C++ значение правильно преобразуется в аналог на OS.

Часть 4: биндинг пользовательских классов и функций на C++

В части 3 был описан низкоуровневый способ подключения, он сохранился. Новый способ реализует магию по подключению функций с любыми параметрами, любым возвращаемым значением. Итак поехали!

Подключение глобальной функции

Пусть у нас на С++ есть следующая функция:

std::string getcwdString()
{
	const int PATH_MAX = 1024;
	char buf[PATH_MAX];
	getcwd(buf, PATH_MAX);
	return buf;
}

Чтобы подключить ее в глобальное пространство имен на OS, нужно выполнить код:

os->setGlobal(def("getcwd", getcwdString));

Вызываем фунцию из ObjectScript:

print "getcwd: "..getcwd()

Вывод:

getcwd: C:SourcesOSproj.win32osbind

Подключение модуля с функциями

Пусть у нас есть следующие функции на С++ (обратите внимание на то, что функции принимают и возвращают совершено разные типы данных):

bool my_isdigit(const OS::String& str)
{
	int len = str.getLen();
	for(int i = 0; i < len; i++){
		if(!isdigit(str[i])){
			return false;
		}
	}
	return len > 0;
}

std::string my_hash(const char * str)
{
	int i, len = strlen(str), hash = 5381;
	for(i = 0; i < len; i++){
		hash = ((hash << 5) + hash) + str[i];
	}
	hash &= 0x7fffffff;
	char buf[16];
	for(i = 0; hash > 0; hash >>= 4){
		buf[i++] = "0123456789abcdef"[hash & 0xf];
	}
	buf[i] = 0;
	return buf;
}

void my_print_num(int i)
{
	printf("my_print_num: %dn", i);
}

void my_print_void(void)
{
	printf("my_print_voidn");
}

long double my_fabs(long double a)
{
	return a >= 0 ? a : -a;
}

Конечно, пользовательские функции могут принимать множество параметров. Подключаем функции к OS как модуль my:

OS::FuncDef funcs[] = {
	def("isdigit", my_isdigit),
	def("hash", my_hash),
	def("print_num", my_print_num),
	def("print_void", my_print_void),
	def("abs", my_fabs),
	{}
};
os->getModule("my");
os->setFuncs(funcs);
os->pop();

Готово, теперь их можно использовать в OS:

print "isdigit(123): "..my.isdigit("123")
print "isdigit(123q): "..my.isdigit("123q")
print "my.hash(123): "..my.hash(123)
print "call my.print_num(123.5)"
my.print_num(123.5)
print "call my.print_void()"
my.print_void()
print "my.abs(-12): "..my.abs(-12)
print "my.fabs(-123.5): "..my.fabs(-123.5)

Вывод:

isdigit(123): true
isdigit(123q): false
my.hash(123): bf9878b
call my.print_num(123.5)
my_print_num: 123
call my.print_void()
my_print_void
my.abs(-12): 12
my.fabs(-123.5): 123.5

Подключение класса на C++

Тут начинается самое интересное. Предполжим, у нас есть следующий тестовый класс на C++, который мы хотим использовать в коде на OS:

class TestClass
{
public:
	int i;
	float j;

	TestClass(int _i, float _j){ i = _i; j = _j; }

	int getI() const { return i; }
	void setI(int _i){ i = _i; }
	
	float getJ() const { return j; }
	void setJ(float _j){ j = _j; }

	double doSomething(int a, float b, double c, TestClass * pb)
	{
		return i + j + a + b + c + pb->i + pb->j;
	}

	void print()
	{
		printf("test class: %d, %fn", i, j);
	}
};

Подключаем к OS:

// 1. нужно объявить класс в пространстве имен ObjectScript
//    OS_DECL_USER_CLASS - это макрос, в котором объявляются несколько 
//    служебных функций для правильной типизации класса на C++
namespace ObjectScript { OS_DECL_USER_CLASS(TestClass); }

// 2. нужно сделать функцию, которая будет создавать экземпляр класса
TestClass * __constructTestClass(int i, float j){ return new TestClass(i, j); }

// 3. описать протопит класса и зарегистрировать его в OS
OS::FuncDef funcs[] = {
	def("__construct", __constructTestClass),
	def("__get@i", &TestClass::getI),
	def("__set@i", &TestClass::setI),
	def("__get@j", &TestClass::getJ),
	def("__set@j", &TestClass::setJ),
	def("doSomething", &TestClass::doSomething),
	def("print", &TestClass::print),
	{}
};
registerUserClass<TestClass>(os, funcs);

Готово, проверяем в OS:

var t = TestClass(1, 0.25)
print "t.i: "..t.i
print "t.j: "..t.j

var t2 = TestClass(2, 0.5)
t2.i = t2.i + t.j
print "t2"
t2.print()

print "t.doSomething(10, 100.001, 1000.1, t2): "..t.doSomething(10, 100.001, 1000.1, t2)

Вывод:

t.i: 1
t.j: 0.25
t2
test class: 2, 0.500000
t.doSomething(10, 100.001, 1000.1, t2): 1113.8509994506835

Работает! В исходниках к данной статье вы также найдете, как клонировать пользовательский класс и перегружать математические операторы.

Подключение пользовательского типа данных на C++

Ну и на закуску, предположим у нас в C++ есть структура данных и мы хотим, чтобы она выглядела в OS, как контейнер со значениями.

struct TestStruct
{
	float a, b;

	TestStruct(){ a = b = 0; }
	TestStruct(float _a, float _b){ a = _a; b = _b; }
};
void printTestStruct(const TestStruct& p)
{
	printf("TestStruct: %f %fn", p.a, p.b);
}
TestStruct changeTestStruct(const TestStruct& p)
{
	return TestStruct(p.a*10, p.b*100);
}

Давайте научим OS работать с нашей структурой (передавать ее в качестве параметра и возвращать результат):

namespace ObjectScript {

OS_DECL_USER_CLASS(TestStruct);

template <>
struct CtypeValue<TestStruct>
{
	// type используется внутри OS
	typedef TestStruct type;
	// возвращает true, если функция на C++ может работать с полученным значением
	static bool isValid(const TestStruct&){ return true; }
	// если параметр не был передан из OS, то возвращается def
	static TestStruct def(ObjectScript::OS * os){ return TestStruct(0, 0); }
	// считывание параметра из стека OS
	static TestStruct getArg(ObjectScript::OS * os, int offs)
	{
		if(os->isObject(offs)){
			os->getProperty(offs, "a"); // required
			float a = os->popFloat();
		
			os->getProperty(offs, "b"); // required
			float b = os->popFloat();

			return TestStruct(a, b);
		}
		os->triggerError(OS_E_ERROR, "TestStruct expected");
		return TestStruct(0, 0);
	}
	// учим OS пушить в стек значения типа TestStruct
	static void push(ObjectScript::OS * os, const TestStruct& p)
	{
		os->newObject();
	
		os->pushStackValue();
		os->pushNumber(p.a);
		os->setProperty("a");
				
		os->pushStackValue();
		os->pushNumber(p.b);
		os->setProperty("b");
	}
};

} // namespace ObjectScript

Регистрируем функции на C++ для работы c TestStruct в глобальном пространстве имен OS:

os->setGlobal(def("printTestStruct", printTestStruct));
os->setGlobal(def("changeTestStruct", changeTestStruct));

Проверяем в OS:

var data = {a=10 b=20}
printTestStruct(data)
data = changeTestStruct(data)
printTestStruct(data)
print data

Вывод:

TestStruct: 10.000000 20.000000
TestStruct: 100.000000 2000.000000
{"a":100,"b":2000}

Отлично, все работает! Все простые типы данных (float, int и т.п.) уже описаны в OS через CtypeValue аналогичным образом. Используйте CtypeValue, если вам нужно описать специфическую конвертацию типа данных OS -> C++ и наоборот.

Вы можете скачать исходники ObjectScript и пример из данной статьи по этой ссылке, открыть proj.win32examples.sln, проект osbind.

Другие релевантные статьи об ObjectScript:

Автор: evgeniyup

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


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