Запускаем vs2013 (у меня community).
Обычно после этого нужно создавать проект или открывать существующий. Т.к. мы начинаем с нуля, то будем создавать (Файл\Создать\Проект).
В открывшееся окно обычно не вызывает застревания, но мы проговорим и это. Выбираем в дереве "Visual C++", а в списке "Консольное приложение Win32". Теперь трудный момент - нужно придумать название или имя. Проблема обычно в том, что мы не знаем, чего хотим.
Давайте назовем проект SuperI. Нажимаем "Ок" и открывается мастер. К счастью, на первой закладке ничего не нужно вводить и отмечать. Жмем "Далее".
Со второй закладкой все совсем иначе (уже что-то заполнено, а мы еще мало что понимаем). Переставляем отметку с "Консольное приложение" на "Библиотека DLL". Не спешите нажимать "Готово", мы ведь хотим совсем с нуля, правда? Надо отметить пустой проект, а уже потом нажимать "Готово".
Ну вот, это оно - наш пустой проект. В этом месте обычно вспоминают о дисках ИТС и прочем. Нам понадобится кое-что добавить. Прикрепил это к публикации. Лучше сложить файлы в каталог <путь до>\Documents\Visual Studio 2013\ProjectsCPP\SuperI\SuperI, а потом добавить их в проект.
Добавить нужно ряд заголовочных файлов (Проект\Добавить существующий элемент):
1. adapter.h - Базовый интерфейс объекта платформы (для взаимодействия с платформой 1С)
2. memory_adapter.h - Предопределенный класс, для выделения и освобождения памяти в платформе
3. base.h - интерфейс для инициализации компоненты, предопределенный интерфейс описания методов предназначенный для использования платформой 1С, интерфейс для изменения локализации в компоненте
4. com.h
5. types.h
Добавим еще и файл исходного кода (Проект\Добавить существующий элемент):
1. dllmain.cpp - стандартная точка входа в библиотеку
Увы и ах, но это еще не конец. Нам нужен манифест. Делаем файл с именем MANIFEST.XML в папке с проектом <путь до>\Documents\Visual Studio 2013\ProjectsCPP\SuperI (в этом каталоге лежит папка SuperI с нашим исходным кодом).
А вот и содержание этого файла:
<?xml version="1.0" encoding="UTF-8"?>
<bundle xmlns="http://v8.1c.ru/8.2/addin/bundle">
<component os="Windows" path="SuperI.dll" type="native" arch="i386" />
<component os="Windows" path="SuperI64.dll" type="native" arch="x86_64"/>
<component os="Linux" path="SuperI.so" type="native" arch="i386"/>
<component os="Linux" path="libSuperI64.so" type="native" arch="x86_64"/>
</bundle>
Важно не перепутать содержание атрибута path! Заметили имя своего проекта в имени конечной библиотеки?
Теперь немного муторное дело - нужно подготовить последний магический файлик с именем SuperI.def в папке исходного кода <путь до>\Documents\Visual Studio 2013\ProjectsCPP\SuperI\SuperI
Содержимое файла (обратите внимание на имя SuperI):
LIBRARY "SuperI"
EXPORTS
GetClassObject
DestroyObject
GetClassNames
В студии есть "Обозреватель решений". Обычно он даже открыт, но если Вы закрываете всё лишнее, то можно открыть его снова Вид\Обозреватель решений. В окне обозревателя решений в самом верху дерева указан проект, а ниже наше решение "SuperI". Пот по решению и щелкаем правой кнопкой (кнопка на мышке). В самом низу контекстного меню есть пункт "Свойства". Выбираем его.
В открывшемся окне слева опять дерево. Пропускаем "Общие свойства" и переходим "Свойства конфигурации\Компоновщик\Все параметры". В списке (в самом низу почти) есть пункт "Файл определения модуля". Он-то нам и нужен! Нужно изменить его значение на текст: SuperI.def
НАЖИМАЕМ КНОПКУ "OK".
Все, магия кончилась.
На этом месте у Вас уже есть вполне рабочий инструмент - базовый компонент, который можно наследовать со своей реализацией. Пример такой реализации мы в начале добавим, а позже разберем.
Добавляем файл class.hpp в папку с исходными тестами <путь до>\Documents\Visual Studio 2013\ProjectsCPP\SuperI\SuperI
Теперь можно собрать проект "Сборка\Собрать решение"
У меня получился такой результат:
Это не значит, что у Вас то же самое. Но если получилось успешно собрать, то поздравляю!
Дальше идем в 1С и пишем там такой код:
&НаКлиенте
перем ДемоКомп;
&НаКлиенте
Процедура ВнешнееСобытие(Источник, Событие, Данные)
Сообщить(Источник + " " + Событие + " " + Данные);
КонецПроцедуры
&НаКлиенте
Процедура ПриОткрытии(Отказ)
res=ПодключитьВнешнююКомпоненту("C:\Users\etyurin\Documents\Visual Studio 2013\ProjectsCPP\SuperI\Debug\SuperI.dll", "VK", ТипВнешнейКомпоненты.Native);
ДемоКомп = Новый("AddIn.VK.myClass");
ДемоКомп.Свойство_int=-59;
ДемоКомп.Свойство_double=-10.6598;
ДемоКомп.Свойство_pchar="увфцв sdfd 4545";
врем=ДемоКомп.Свойство_pchar;
ДемоКомп.Свойство_bool=Ложь;
ДемоКомп.Свойство_tm=ТекущаяДата();
ДемоКомп.Процедура1(-29);
res=ДемоКомп.Функция2(-1,-9000);
res2=ДемоКомп.Функция1();
КонецПроцедуры
Проверяйте работу в отладчике 1С.
Грабли:
1) Нет манифеста
2) Нет магического файла
3) Проект не библиотеки (просто поменяете в свойствах решения "свойства конфигурации\общие" - в списке "тип конфигурации" установите "Динамическая библиотека (.dll)", не забываем нажимать "Ok")
Теперь пришло время немного внимательней разобраться. Лучше сразу освоить отладку компоненты в студии. Меню "Отладка\Присоединиться к процессу"
Откроется список процессов, вызовы из которых можно перехватить:
После этого на Ваших точках останова программа будет тормозиться. И можно выяснить в чем же проблема то.
Когда Вы пишете в 1С:
res=ПодключитьВнешнююКомпоненту("C:\Users\etyurin\Documents\Visual Studio 2013\ProjectsCPP\SuperI\Debug\SuperI.dll", "VK", ТипВнешнейКомпоненты.Native);
то первым делом будет запущена функция:
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
Что напряглись? Расслабьтесь в это вникать не нужно.
Как 1С узнает, что в компоненте присутствует? - Ответ 1С получит вызвав функцию GetClassNames():
/*Список доступных типов*/
static const wchar_t Class_Names[] = L"myClass"; //|OtherClass1|OtherClass2
/*Получение списка возможных типов*/
const WCHAR_T* GetClassNames() {
static WCHAR_T* names;
wchar_to_WCHAR(names, Class_Names);
return names;
}
.
Class_Names - это просто массив символов, где через вертикальную черту перечислены все возможные имена классов или их псевдонимы. Функция GetClassNames просто возвращает в 1С эту строку.
1С будет создавать экземпляры каждого типа сразу при подключении, а потом будет использовать их. Если вызвать первый раз Новый("AddIn.VK.myClass"), то будет использован уже созданный при подключении экземпляр, а если вызвать снова Новый("AddIn.VK.myClass") - то будет создан уже новый экземпляр класса.
Получив список возможных имен или псевдонимов классов, платформа, не задумываясь более, создает экземпляр каждого из них вызывая функцию GetClassObject:
long GetClassObject(const WCHAR_T* ex_name, Base** pInterface) {
if(!*pInterface) {
wchar_t * name = nullptr;
WCHAR_to_wchar(name, ex_name);
if(!wcscmp(name, L"myClass"))
*pInterface = new myClass();
delete[] name;
return (long)*pInterface;
}
return 0;
}
Функция GetClassObject устроена совсем примитивно - в ней указано соответствие между именем и классом. Она создает экземпляр и возвращает ссылку на него платформе.
Недостатком типового "решения" 1С является его ориентация не на быструю разработку, а на быстрое понимание принципов работы платформы с компонентой. Поэтому здесь Вы не найдете этого типового примера.
Когда создается экземпляр класса, то вызывается его конструктор:
/*конструктор*/
myClass() {
/*объявляем имя класса доступное из 1С*/
Base::fill_name(L"myClass");
/*объявляем свойства доступные из 1С*/
Base::Prop Props[] = {
{L"Свойство_int" , L"Prorp0", true, true},
{L"Свойство_double", L"Prorp1", true, true},
{L"Свойство_pchar" , L"Prorp2", true, true},
{L"Свойство_bool" , L"Prorp3", true, true},
{L"Свойство_tm" , L"Prorp4", true, true}
};
Base::fill_props(Props, sizeof(Props) / sizeof(Base::Prop));
/*объявляем методы доступные из 1С*/
Base::Method Methods[] = {
{L"Функция1", L"Func1", 0},
{L"Функция2", L"Func2en", 2},
{L"Процедура1", L"Proc1en", 1, true}
};
Base::fill_methods(Methods, sizeof(Methods) / sizeof(Base::Method));
Prop2 = new char[100];
std::strcpy(Prop2, "abc абС 123");
time_t rawtime;
time(&rawtime);
Prop4 = *localtime(&rawtime);
}
Я искал подходящую реализацию. Теперь я могу о ней рассказать. Первым делом, мы свяжем экземпляр нашего класса с тем псевдонимом, о котором известно 1С:
/*объявляем имя класса доступное из 1С*/
Base::fill_name(L"myClass");
Платформа обязательно спросит его снова, и нам нужно не забыть вернуть то же наименование. Чтобы разобраться в назначение последующего кода нужно держать в голове схему опроса платформой возможностей экземпляра класса. Платформу будет интересовать количество доступных свойств (GetNProps). Зная количество 1С будет запрашивать свойства по их индексам (начиная с 0). Имя свойства (GetPropName), можно ли прочитать значение свойства (IsPropReadable), можно ли изменить (IsPropWritable), получить значение свойства (GetPropVal) или установить значение (SetPropVal). Во всех этих случаях платформа будет передавать индекс. А вот тип значения 1С не беспокоит - вы получите коробку, в которой может быть сразу любой тип (Ваша компонента будет решать, что с этой коробкой делать).
/*объявляем свойства доступные из 1С*/
Base::Prop Props[] = {
{L"Свойство_int" , L"Prorp0", true, true},
{L"Свойство_double", L"Prorp1", true, true},
{L"Свойство_pchar" , L"Prorp2", true, true},
{L"Свойство_bool" , L"Prorp3", true, true},
{L"Свойство_tm" , L"Prorp4", true, true}
};
Base::fill_props(Props, sizeof(Props) / sizeof(Base::Prop));
В констукторе экземпляра заполняется массив структур Props. С каждым свойством связано два имени (русское и английское) и два свойства возможность получить значение и возможность изменить значение. Так они и перечислены:
{L"Русское_Имя", L"Английское_Имя", <возможность получить значение> , < возможность изменить значение>}
Аналогично обстоят дела и с функциями/процедурами. Также все методы вызываются по номерам:
/*объявляем методы доступные из 1С*/
Base::Method Methods[] = {
{L"Функция1", L"Func1", 0},
{L"Функция2", L"Func2en", 2},
{L"Процедура1", L"Proc1en", 1, true}
};
Base::fill_methods(Methods, sizeof(Methods) / sizeof(Base::Method));
Требуется указать русское имя, английское, количество параметров и признак процедуры:
{L"Русское_Имя", L"Английское_Имя", <количество параметров> , <признак процедуры>}
Типы параметров платформу не интересуют. Она просто будет следить за нужным их количеством. Все параметры будут переданы массивом коробок, с указанием количества элементов в массиве. Если признак процедуры равен true, то возвращаемое значение в 1С передать не получится.
Вот и весь конструктор, который требуется оформить.
Посмотрим на детали обращения к свойствам (с методами повторится тот же алгоритм). Когда в коде 1С мы пишем:
врем=ДемоКомп.Свойство_pchar;
то платформе понадобится номер свойства (ведь в коде только имя). Немедленно последует вызов функции FindProp:
long Base::FindProp(const WCHAR_T* ex_name) {
long res = -1;
wchar_t * temp = nullptr;
WCHAR_to_wchar(temp, ex_name);
for(long i = 0; res == -1 && i < cnt_props; ++i)
if(!wcscmp(Props[i]->en, temp) || !wcscmp(Props[i]->ru, temp))
res = i;
delete[] temp;
return res;
}
Эта функция просто вернет индекс свойства из массива Props (помните в конструкторе заполняли?) Получив индекс платформа будет выяснять возможность прочитать свойство IsPropReadable:
bool Base::IsPropReadable(const long num) {
if(num < cnt_props)
return Props[num]->r;
else
return false;
}
Функция просто вернет соответствующее поле структуры свойств Props. Если свойство можно прочитать, то от платформы последует вызов GetPropVal:
/*Получение свойства*/
virtual bool ADDIN_API GetPropVal(const long num, tVariant* var) override {
switch(num) {
case 0: //Свойство_int
TV_VT(var) = VTYPE_I4; //выставляем тип
TV_I4(var) = Prop0; //выставляем значение
break;
case 1: //Свойство_double
TV_VT(var) = VTYPE_R8; //выставляем тип
TV_R8(var) = Prop1; //выставляем значение
break;
case 2: //Свойство_pchar
TV_VT(var) = VTYPE_PSTR; //выставляем тип
var->pstrVal = Prop2; //сразу указатель на строку
var->strLen = std::strlen(Prop2);
break;
case 3: //Свойство_bool
TV_VT(var) = VTYPE_BOOL; //выставляем тип
TV_BOOL(var) = Prop3; //выставляем значение
break;
case 4: //Свойство_tm
TV_VT(var) = VTYPE_TM; //выставляем тип
var->tmVal = Prop4; //выставляем значение
break;
default:
return false;
}
return true;
}
Функция заполняет коробку tVariant* согласно индекса переданного свойства. Сама коробка в деталях описана в types.h
Если в коде 1С написать:
ДемоКомп.Свойство_int=-59;
То последуют вызовы FindProp и IsPropWritable (можно ли изменять свойство):
bool Base::IsPropWritable(const long num) {
if(num < cnt_props)
return Props[num]->w;
else
return false;
}
Наша функция просто вернет соответствующее поле структуры Props, по указанному платформой индексу. Если свойство можно менять, то последует вызов SetPropVal с указанием индекса свойства:
/*Установка свойства*/
virtual bool ADDIN_API SetPropVal(const long num, tVariant * var) override {
switch(num) {
case 0:
if(TV_VT(var) != VTYPE_I4)
return false;
Prop0 = TV_I4(var);
break;
case 1:
if(TV_VT(var) != VTYPE_R8)
return false;
Prop1 = TV_R8(var);
break;
case 2:
if(TV_VT(var) == VTYPE_PSTR) {
delete[] Prop2;
size_t len = std::strlen(var->pstrVal);
Prop2 = new char[len + 1];
std::strncpy(Prop2, var->pstrVal, len + 1);
break;
} else if(TV_VT(var) == VTYPE_PWSTR) {
delete[] Prop2;
WCHAR_to_char(Prop2, var->pwstrVal);
break;
} else
return false;
case 3:
if(TV_VT(var) != VTYPE_BOOL)
return false;
Prop3 = TV_BOOL(var);
break;
case 4:
if(TV_VT(var) != VTYPE_TM)
return false;
Prop4 = var->tmVal;
break;
default:
return false;
}
return true;
}
Платформа "любит" передавать строки через указатель WCHAR_T*
Ну что страшно пока? Ничего, скоро поймете, что самое страшное я за Вас сделал и Вам осталось только приятная часть. Кстати со свойствами больше ничего и нет. Они перечислены как члены данных в классе:
private:
int Prop0 = -113;
double Prop1 = 7.65;
char * Prop2 = nullptr;
bool Prop3 = true;
tm Prop4;
Полегчало?
Вы можете просто написать такой класс:
class myClass: public Base {
public:
/*конструктор*/
myClass() {
/*объявляем имя класса доступное из 1С*/
Base::fill_name(L"myClass");
}
};
и экземпляр его уже можно создать строчкой:
ДемоКомп = Новый("AddIn.VK.myClass");
только он совсем пустой и ничего не делает.
Пришла пора взглянуть на методы. Это процедуры и функции. Сведения о них платформа получает последовательно. В начале - общее количество всех методов GetNMethods, потом количество формальных параметров конкретного метода GetNParams (просто по индексу, как обычно), дальше платформа захочет выяснить нужно ли ей принимать возвращаемое значение HasRetVal. И в конце последует вызов метода CallAsProc (для процедуры) или CallAsFunc(для функции). Вот смотрите пример:
/*Методы*/
virtual bool ADDIN_API CallAsProc(const long num, tVariant* paParams, const long len) override {
bool res = false;
if(num < cnt_methods && Methods[num]->is_proc) {
switch(num) {
case 2:
res = Proc1(paParams, len);
break;
default:
res = false;
}
}
return res;
}
virtual bool ADDIN_API CallAsFunc(const long num, tVariant* pvarRetValue, tVariant* paParams, const long len) override {
bool res = false;
if(num < cnt_methods && !Methods[num]->is_proc) {
switch(num) {
case 0:
res = Func1(pvarRetValue, paParams, len);
break;
case 1:
res = Func2(pvarRetValue, paParams, len);
break;
default:
res = false;
}
}
return res;
}
bool Proc1(tVariant* paParams, const long lSizeArray) {
bool res = true;
if(paParams[0].vt == VTYPE_I4) //Проверка, что пришло целое число
this->Prop0 = paParams[0].lVal; //значение
else
res = false;
return res;
}
bool Func2(tVariant* pvarRetValue, tVariant* paParams, const long len) {
bool res = true;
int a,b;
if(paParams[0].vt == VTYPE_I4) //Проверка, что пришло целое число
a = paParams[0].lVal; //значение
else
res = false;
if(paParams[1].vt == VTYPE_I4) //Проверка, что пришло целое число
b=paParams[1].lVal; //значение
else
res = false;
if(res) {
pvarRetValue->vt = VTYPE_I4; //указываем возвращаемый тип
pvarRetValue->lVal = a + b; //указываев возвращаемое значение (выполнять pMemoryAdapter->AllocMemory((void**)&pvarRetValue->lVal, size_in_byte) здесь не требуется, т.к. передаем значение)
}
return res;
}
bool Func1(tVariant* pvarRetValue, tVariant* paParams, const long len) {
bool res = true;
size_t l = std::strlen(Prop2) + 1;
pvarRetValue->vt = VTYPE_PSTR;
pvarRetValue->strLen = l;
/*нужно аллоцировать место в структуре возвращаемой в 1С*/
pMemoryAdapter->AllocMemory((void**)&pvarRetValue->pstrVal, l*sizeof(char));
std::memcpy(pvarRetValue->pstrVal, Prop2, l*sizeof(char));
return res;
}
Платформа передает индекс метода, коробку с параметрами и количество элементов в коробке. Вам не нужно беспокоиться о количестве элементов (будет ровно столько, сколько вы указали в конструкторе). А вот с выковыриванием нужных Вам значений из коробок придется постараться. Для этого обязательно выясняйте значение поля vt, чтоб знать как извлечь значение.
Как только в 1С переменная ДемоКомп покинет границы видимости, сразу последует вызов DestroyObject:
/*Уничтожение экземпляра*/
long DestroyObject(Base** pIntf) {
if(!*pIntf)
return -1;
delete *pIntf;
*pIntf = 0;
return 0;
}
Платформа так сообщает компоненте, что экземпляр больше не нужен.
Заметили, что компонента с типом Native не требует регистрации?