gifts2017

Опыты с автоматическим переводом кода C# в 1С-код через Roslyn

Опубликовал Сергей Карташев (Elisy) в раздел Программирование - Практика программирования

Появилась идея посмотреть, как будет выглядеть объектно-ориентированный подход в 1С, язык которой очень ограничен в средствах и не предусматривает определение классов. Программа по автоматическому переводу определений классов C# в другой язык позволила бы менять генерируемый код по мере появления новых идей. Поиски средств реализации привели к проекту Roslyn - открытому компилятору C#.

Появилась идея посмотреть, как будет выглядеть объектно-ориентированный подход в 1С, язык которой очень ограничен в средствах и не предусматривает определение классов. Программа по автоматическому переводу определений классов C# в другой язык позволила бы менять генерируемый код по мере появления новых идей. Поиски средств реализации привели к проекту Roslyn – открытому компилятору C#.

Roslyn – это открытая платформа компиляции C# и Visual Basic. Roslyn выполняет два основных действия: строит синтаксическое дерево (парсинг) и компилирует синтаксическое дерево. Дополнительно позволяет анализировать исходный код, рекурсивно обходить его, работать с проектами Visual Studio, выполнять код на лету.

Обратите внимание, что на данный момент Roslyn в стадии Бета. Исходя из этого, со временем в компиляторе может что-то поменяться.

Roslyn – открытый компилятор C#



Подключить Roslyn в проект можно через Nuget:

Install-Package Microsoft.CodeAnalysis –Pre 


Для удобства в коде лучше сразу подключить три пространства имен

using Microsoft.CodeAnalysis.CSharp; 
using Microsoft.CodeAnalysis.CSharp.Syntax; 
using Microsoft.CodeAnalysis; 


Получить синтаксическое дерево кода из строки (или файла) можно так:

SyntaxTree tree = CSharpSyntaxTree.ParseText(codeString); 


Синтаксическое дерево представляет из себя иерархию объектов, наследованных от SyntaxNode. Объекты созданы на все случаи жизни. Примеры: ClassDeclarationSyntax — определение класса, NamespaceDeclarationSyntax – определение пространства имен, PropertyDeclarationSyntax – определение свойства, AccessorDeclarationSyntax – определение метода доступа к свойству (get/set), BlockSyntax – содержимое блока (между фигурными скобками), ExpressionStatementSyntax – выражение и т.д.

Если есть задача рекурсивно пройти все элементы дерева, можно создать свой класс Walker и наследовать его от CSharpSyntaxWalker. Базовый класс позволяет переопределять общий метод Visit(SyntaxNode node) или большое множество специализированных, вида: void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node), void VisitClassDeclaration(ClassDeclarationSyntax node), void VisitConstructorDeclaration(ConstructorDeclarationSyntax node) и т.д. Не забывайте вызвать в каждом переопределенном методе базовый метод, чтобы не останавливать рекурсии.

Вызов рекурсивного обхода можно запустить следующим образом:

var walker = new Walker(); walker.Visit(tree.GetRoot()); 


В синтаксическом дереве нет информации о типах. Информация об используемых типах появляется после вызова:

var compilation = CSharpCompilation.Create("1ccode").WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)).AddReferences(new MetadataFileReference(typeof(object).Assembly.Location)).AddSyntaxTrees(tree); 
var Model = compilation.GetSemanticModel(tree); 


После этого вызова можно получать информацию об используемых типах, вызывая, например для класса

var classSymbol = Model.GetDeclaredSymbol(classDeclarationSyntax); 


Теперь, имея информацию, о типе, можно узнать какой класс унаследовал данный тип:

var type = type.BaseType; 


Или получить все члены типа через type.GetMembers()

Автоматический перевод кода C# в код 1С



Код не претендует на полноту и правильность, так как имеет цель получить общее представление об ООП-подходе в 1С.

Для перевода C#-кода в код 1С был создан класс Walker, наследованный от CSharpSyntaxWalker. Walker перебирает все определения и строит на выходе 1С-код.

Класс производит следующие преобразования.

Пространство имен переводится методом VisitNamespaceDeclaration в модуль 1С, где точки в названии заменены на знаки подчеркивания.

Понятия класс в 1С нет, поэтому определение класса в методе VisitClassDeclaration пропускается. Имя класса будет присутствовать в названии каждой функции и процедуры 1С, чтобы обозначить принадлежность к одному типу. Присутствующие в базовых классах методы, но отсутствующие в текущем классе через DeclareBaseClassMethodsToImplement и DeclareBaseClassPropertiesToImplement определяются с вызовом «базовых» функций/процедур 1С.

Конструкторы в VisitConstructorDeclaration переводятся в определения функций 1С с именем класса, первым параметром _this и списком параметров. Если нет вызова другого конструктора этого класса, происходит инициализация всех полей класса в структуре. Определяется вызов других конструкторов.

Определение свойств в VisitPropertyDeclaration пропускаются. Важны определения их методов доступа.

Методы доступа свойств в VisitAccessorDeclaration переводятся в определения с именами <название класса>_Получить_<имя свойства> и <название класса>_Установить_<имя свойства>. Если они авто-реализованные (auto-implemented), то генерируется код доступа к переменной _this._private_<название класса>_<имя свойства>.

Для методов в VisitMethodDeclaration генерируются определения 1С-процедур.

Выражения и «возвраты» в VisitExpressionStatement и VisitReturnStatement комментируются через // и вставляются в текст как есть.

Исходный код Walker.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis;
namespace Roslyn
{
    public class Walker : CSharpSyntaxWalker
    {
        SyntaxTree Tree { get; set; }
        CSharpCompilation Compilation { get; set; }
        SemanticModel Model { get; set; }
        TextWriter Writer { get; set; }
        public Walker(TextWriter writer, SyntaxTree tree, CSharpCompilation compilation) : base()
        {
            Writer = writer;
            Tree = tree;
            Compilation = compilation;
            Model = Compilation.GetSemanticModel(tree);
        }
        Dictionary<classdeclarationsyntax, fielddeclarationsyntax[]> _classFields = new Dictionary<classdeclarationsyntax, fielddeclarationsyntax[]>();
        NamespaceDeclarationSyntax _currentNamespace;
        ClassDeclarationSyntax _currentClass;
        PropertyDeclarationSyntax _currentProperty;
        private int Tabs = 0;
        public override void Visit(SyntaxNode node)
        {
            //Tabs++;
            //var indents = new String('\t', Tabs);
            //Writer.WriteLine(indents + node.GetType().Name + "/" + node.CSharpKind());
            base.Visit(node);
            //Tabs--;
        }
        public override void VisitNamespaceDeclaration(NamespaceDeclarationSyntax node)
        {
            _currentNamespace = node;
            Writer.WriteLine("Модуль " + node.Name.ToString().Replace(".", "_"));
            base.VisitNamespaceDeclaration(node);
        }
        public override void VisitClassDeclaration(ClassDeclarationSyntax node)
        {
            _currentClass = node;
            var fields = node.ChildNodes().OfType<fielddeclarationsyntax>().ToArray();
            _classFields[node] = fields;
            Writer.WriteLine();
            Writer.WriteLine(string.Format("//Класс {0}", node.Identifier));
            base.VisitClassDeclaration(node);
            DeclareBaseClassPropertiesToImplement(node);
            DeclareBaseClassMethodsToImplement(node);
        }
        void DeclareBaseClassMethodsToImplement(ClassDeclarationSyntax classNode)
        {
            var classSymbol = Model.GetDeclaredSymbol(classNode);
            List<string> processedMembers = new List<string>();
            var type = classSymbol;
            while (type != null)
            {
                foreach(var member in type.GetMembers())
                {
                    var declarators = member.DeclaringSyntaxReferences;
                    if (declarators == null || declarators.Length == 0)
                        continue;
                    if (declarators.Length != 1)
                        throw new NotImplementedException();
                    var memberNode = declarators[0].GetSyntax() as MethodDeclarationSyntax;
                    if (memberNode == null)
                        continue;
                    if (processedMembers.Any(m=>m == member.Name))
                        continue;
                    processedMembers.Add(member.Name);
                    if (type == classSymbol)
                        //Skip original class members. Declare only base classes
                        continue;
                    Writer.WriteLine();
                    Writer.WriteLine(string.Format("Процедура {0}_{1}(_this)", _currentClass.Identifier, memberNode.Identifier));
                    Writer.WriteLine(string.Format("    {0}_{1}(_this);", type.Name, member.Name));
                    Writer.WriteLine(string.Format("КонецПроцедуры;"));
                }
                type = type.BaseType;
            }
        }
        void DeclareBaseClassPropertiesToImplement(ClassDeclarationSyntax classNode)
        {
            var classSymbol = Model.GetDeclaredSymbol(classNode);
            List<string> processedMembers = new List<string>();
            var type = classSymbol;
            while (type != null)
            {
                foreach(var member in type.GetMembers())
                {
                    var declarators = member.DeclaringSyntaxReferences;
                    if (declarators == null || declarators.Length == 0)
                        continue;
                    if (declarators.Length != 1)
                        throw new NotImplementedException();
                    var memberNode = declarators[0].GetSyntax() as PropertyDeclarationSyntax;
                    if (memberNode == null)
                        continue;
                    if (processedMembers.Any(m => m == memberNode.Identifier.ToString()))
                        continue;
                    processedMembers.Add(memberNode.Identifier.ToString());
                    if (type == classSymbol)
                        //Skip original class members. Declare only base classes
                        continue;
                    Writer.WriteLine();
                    Writer.WriteLine(string.Format("Функция {0}_Получить_{1}(_this)", _currentClass.Identifier, memberNode.Identifier));
                    Writer.WriteLine(string.Format("    Возврат {0}_Получить_{1}(_this);", type.Name, member.Name));
                    Writer.WriteLine(string.Format("КонецФункции;"));
                    Writer.WriteLine();
                    Writer.WriteLine(string.Format("Процедура {0}_Установить_{1}(_this, value)", _currentClass.Identifier, memberNode.Identifier));
                    Writer.WriteLine(string.Format("    {0}_Установить_{1}(_this);", type.Name, member.Name));
                    Writer.WriteLine(string.Format("КонецПроцедуры;"));
                }
                type = type.BaseType;
            }
        }
        public override void VisitConstructorDeclaration(ConstructorDeclarationSyntax node)
        {
            Writer.WriteLine();
            var symbol = Model.GetDeclaredSymbol(node);
            List<string> parameters = new List<string>();
            parameters.Add("_this");
            parameters.AddRange(node.ParameterList.Parameters.Select(m => m.Identifier.ToString()).ToArray());
            Writer.WriteLine(string.Format("Функция {0}({1}){2}", node.Identifier, String.Join(", ", parameters), " Экспорт"));
            Writer.WriteLine();
            Tabs++;
            var indents = new String('\t', Tabs);
            //Initialize members first if no this constructor initializer (:this()) call
            if (!node.DescendantNodes().OfType<constructorinitializersyntax>().Any(m=>m.CSharpKind() == SyntaxKind.ThisConstructorInitializer) && _classFields.ContainsKey(_currentClass))
            {
                Writer.WriteLine(indents + String.Format("//Инициализация полей"));
                //Writer.WriteLine(String.Format("_this = Новый Структура();"));
                foreach (var field in _classFields[_currentClass])
                {
                    Writer.WriteLine(String.Format(indents + "_this.Вставить(\"{0}\", {1})", field.Declaration.Variables[0].Identifier, field.Declaration.Variables[0].Initializer.Value));
                }
            }
            if (node.Initializer != null)
            {
                List<string> arguments = new List<string>();
                arguments.Add("_this");
                arguments.AddRange(node.Initializer.ArgumentList.Arguments.Select(m => m.Expression.ToString()).ToArray());
                if (node.Initializer.ThisOrBaseKeyword.CSharpKind() == SyntaxKind.BaseKeyword)
                {
                    Writer.WriteLine(indents + String.Format("//Вызов конструктора базового класса"));
                    Writer.WriteLine(indents + String.Format("{0}({1});", _currentClass.BaseList.Types[0], String.Join(", ", arguments)));
                }
                else if (node.Initializer.CSharpKind() == SyntaxKind.ThisConstructorInitializer)
                {
                    Writer.WriteLine(indents + String.Format("//Вызов другого конструктора"));
                    Writer.WriteLine(indents + String.Format("{0}({1});", _currentClass.Identifier, String.Join(", ", arguments)));
                }
            }
            Writer.WriteLine(String.Format(indents + "_this.Вставить(\"__type\", \"{0}.{1}\")", symbol.ContainingNamespace.Name, symbol.ContainingType.Name));
            base.VisitConstructorDeclaration(node);
            Tabs--;
            Writer.WriteLine(indents + string.Format("Возврат _this;"));
            Writer.WriteLine(string.Format("КонецФункции; //{0}({1}){2}", node.Identifier, String.Join(", ", parameters), " Экспорт"));
        }
        public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node)
        {
            _currentProperty = node;
            var symbol = Model.GetDeclaredSymbol(node);
            base.VisitPropertyDeclaration(node);
        }
        public override void VisitAccessorDeclaration(AccessorDeclarationSyntax node)
        {
            Writer.WriteLine();
            if (node.CSharpKind() == SyntaxKind.GetAccessorDeclaration)
            {
                Writer.WriteLine(string.Format("Функция {0}_Получить_{1}(_this)", _currentClass.Identifier, _currentProperty.Identifier));
            }
            else if (node.CSharpKind() == SyntaxKind.SetAccessorDeclaration)
            {
                Writer.WriteLine(string.Format("Процедура {0}_Установить_{1}(_this, value)", _currentClass.Identifier, _currentProperty.Identifier));
            }
            Tabs++;
            if (node.Body == null)
            {
                //auto implemented
                var indents = new String('\t', Tabs);
                if (node.CSharpKind() == SyntaxKind.GetAccessorDeclaration)
                {
                    Writer.WriteLine(indents + string.Format("Возврат _this._private_{0}_{1};", _currentClass.Identifier, _currentProperty.Identifier));
                }
                else if (node.CSharpKind() == SyntaxKind.SetAccessorDeclaration)
                {
                    Writer.WriteLine(indents + string.Format("_this._private_{0}_{1} = value;", _currentClass.Identifier, _currentProperty.Identifier));
                }
            }
            base.VisitAccessorDeclaration(node);
            Tabs--;
            if (node.CSharpKind() == SyntaxKind.GetAccessorDeclaration)
            {
                Writer.WriteLine(string.Format("КонецФункции;"));
            }
            else if (node.CSharpKind() == SyntaxKind.SetAccessorDeclaration)
            {
                Writer.WriteLine(string.Format("КонецПроцедуры;"));
            }
        }
        public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
        {
            Writer.WriteLine();
            Writer.WriteLine(string.Format("Процедура {0}_{1}(_this)", _currentClass.Identifier, node.Identifier));
            Tabs++;
            base.VisitMethodDeclaration(node);
            Tabs--;
            Writer.WriteLine(string.Format("КонецПроцедуры;"));
        }
        public override void VisitExpressionStatement(ExpressionStatementSyntax node)
        {
            var indents = new String('\t', Tabs);
            Writer.WriteLine(("\r\n" + node.ToString()).Replace("\r\n", "\r\n" + indents + "//"));
            base.VisitExpressionStatement(node);
        }
        public override void VisitReturnStatement(ReturnStatementSyntax node)
        {
            var indents = new String('\t', Tabs);
            Writer.WriteLine(("\r\n" + node.ToString()).Replace("\r\n", "\r\n" + indents + "//"));
            base.VisitReturnStatement(node);
        }
        //public override void VisitBlock(BlockSyntax node)
        //{
        //    Writer.WriteLine(node.ToString());
        //    base.VisitBlock(node);
        //}
    }
}            
 

 

Результат работы



В итоге код

Исходный код на C#

          namespace ПространствоИмен1.ПИ2
{
    public class А
    {
        public А()
        {
            Свойство1 = "Конструктор А";
        }
        private int _поле1 = 10;        
        public int Поле1 {get {return _поле1;} set {_поле1 = value;}}        
        public string Свойство1 {get; set;}
        public void Метод1()
        {
            Свойство1 = "Метод1";
        }
    }
    public class Б : А
    {
        private int _поле1 = 20;        
        public Б() : base()
        {
            Свойство1 = "Конструктор Б";
            Метод1();
        }
        public Б(int i) : this()
        {
            Свойство1 = "Конструктор Б(int i)";
            Метод1();
        }
    }
}    



Будет переведен в код 1С: Предприятие

Исходный код 1С:Предприятие

Модуль ПространствоИмен1_ПИ2
//Класс А
Функция А(_this) Экспорт
    //Инициализация полей
    _this.Вставить("_поле1", 10)
    _this.Вставить("__type", "ПИ2.А")
    //Свойство1 = "Конструктор А";
    Возврат _this;
КонецФункции; //А(_this) Экспорт
Функция А_Получить_Поле1(_this)
    //return _поле1;
КонецФункции;
Процедура А_Установить_Поле1(_this, value)
    //_поле1 = value;
КонецПроцедуры;
Функция А_Получить_Свойство1(_this)
    Возврат _this._private_А_Свойство1;
КонецФункции;
Процедура А_Установить_Свойство1(_this, value)
    _this._private_А_Свойство1 = value;
КонецПроцедуры;
Процедура А_Метод1(_this)
    //Свойство1 = "Метод1";
КонецПроцедуры;
//Класс Б
Функция Б(_this) Экспорт
    //Инициализация полей
    _this.Вставить("_поле1", 20)
    //Вызов конструктора базового класса
    А(_this);
    _this.Вставить("__type", "ПИ2.Б")
    //Свойство1 = "Конструктор Б";
    //Метод1();
    Возврат _this;
КонецФункции; //Б(_this) Экспорт
Функция Б(_this, i) Экспорт
    //Вызов другого конструктора
    Б(_this);
    _this.Вставить("__type", "ПИ2.Б")
    //Свойство1 = "Конструктор Б(int i)";
    //Метод1();
    Возврат _this;
КонецФункции; //Б(_this, i) Экспорт
Функция Б_Получить_Поле1(_this)
    Возврат А_Получить_Поле1(_this);
КонецФункции;
Процедура Б_Установить_Поле1(_this, value)
    А_Установить_Поле1(_this);
КонецПроцедуры;
Функция Б_Получить_Свойство1(_this)
    Возврат А_Получить_Свойство1(_this);
КонецФункции;
Процедура Б_Установить_Свойство1(_this, value)
    А_Установить_Свойство1(_this);
КонецПроцедуры;
Процедура Б_Метод1(_this)
    А_Метод1(_this);
КонецПроцедуры;
 

См. также

Подписаться Добавить вознаграждение

Комментарии

1. Ийон Тихий (cool.vlad4) 26.12.14 21:18
Elisy +1, но увы не прочитал толком код. Есть же gist в конце концов. Можно было туда код поместить. (не упущу случай похвалить microsoft, в целом они очень радуют в последнее время, бесплатная Visual Studio 2013 Community Edition, планы по выкладыванию в open source Core .Net Framework и т.п. Странно почему изначально Roslyn не сделали и пилили бы на нем все)
2. Сергей Карташев (Elisy) 27.12.14 07:20
(1) cool.vlad4,
Код не особо интересен. Главное, что его можно скопировать к себе, если возникнет необходимость.
3. Сергей Карташев (Elisy) 27.12.14 07:26
Изначально статья была опубликована на Хабре:
http://habrahabr.ru/post/245453/
4. Евгений Матыцин (matytsin_new) 12.01.15 07:39
Интересно, но сложно придумать обоснованное коммерческое применение.
Мой личный опыт интеграции 1с и C# убедил меня в том, что код C# не стоит пытаться транслировать в код 1с. Реализация классов C# не может быть сымитирована в 1с полностью. Полноценного ООП нет. Контроля типов нет. Самодокументирования нет. Код выполняется иначе. После долгих переборов различных способов интеграции .net и 1с я для себя выбрал вариант, когда исходный код модулей на C# тем или иным способом формируется в коде 1с. Затем? с помощью System.Reflection, создается динамическая сборка, содержащая все необходимые модули и эта сборка подгружается как Com-объект. После чего, можно работать с этим объектом в синтаксисе любого .net языка. Способ не идеальный, поскольку COM-соединения иногда ведут себя непредсказуемо (например при разрыве COM-соединения) и дорогой по времени инициализации. Однако это единственный способ, позволяющий на лету формировать свои классы, соответствующие неким уже существующим интерфейсам и разделить разработку кода C# и кода 1c между разными исполнителями с наименьшим риском. Думаю,что использовать интеграцию 1с и .NET на простых классах (без сложной логики внутри классов .net) имеет смысл только для наработки навыков сопряжения вышеозначенных технологий, поскольку простые задачи надежнее решать средствами самой 1с.
5. Сергей Карташев (Elisy) 12.01.15 14:10
(4) matytsin_new,
До коммерческого использования далеко. Скорее пока академический интерес в новогодние каникулы. Продолжение опубликовал только что на Хабре. Суть сводится к тому, что теоретически на Visual Studio можно создать альтернативный 1С конфигуратор.
http://habrahabr.ru/post/247659/
6. serge_focus (serge_focus) 12.01.15 18:16
На самом деле , как мне кажется, подобные интересы вызваны во первых недостаточностью внимания со стороны 1С к разработчикам. "убогая " среда конфигуратора 1С подталкивает на такие эксперименты. Идея понравилась. Так что творческой удачи в экспериментах.
7. Павел Одинцов (Darklight) 13.01.15 16:35
Идея интересная, и действительно, соглашусь с serge_focus, что в будущем может найти и коммерческое применение в альтернативных редактора 1С. Но, остаётся лишь вопрос эффективности получаемого кода. Насколько хорошо он будет работать. Такие конвертации из более мощного языка в менее мощный - всегда чреваты потерей эффективности конкретных алгоритмических реализаций (даже по сравнение с их аналогичной ручной реализацией на конечном языке). Эффективность и гибкость конвертации можно было бы увеличить, если бы сразу конвертировать в байт код машины 1С (что правильнее), как аналог IL-кода, платформы .NET.
И, конечно же, особняком будет стоять проблема отладки. Она тут в крайне затруднена (требует отдельной реализации генератора специфического кода, который встраивал бы отладочные конструкции, которые хоть как-то давали бы обратную связь с жутким снижением производительности). А это ставит очень большой минус для таких разработок. Но, при должном рвении, ресурсов, гениальности и терпения может выйти занятный инструмент. Но, это вряд ли - запал иссякнет раньше, либо не хватит ресурсов. Слишком тяжелый проект, даже для команды.

Была бы интересна и обратная конвертация. Из кода 1С в код C# - для реализации обратного перевода проектов, написанных на 1С в среду .NET framework. Это, как мне кажется, более перспективная и простая в реализации вещь. И оптимизация тут может достичь очень высоких результатов. Вот бы ещё движки присобачить (визуальных форм (в т.ч. печатных) и работы с БД, и метаданными - и была бы хорошая перспектива-альтернатива для 1С).
EliasShy; Elisy; +2 Ответить 1
8. ффф ыыы (zqzq) 14.01.15 08:12
(5) Elisy, 1С и сами планируют альтернативный конфигуратор на эклипс выпустить (к 1С 8.4?), вперёд паровоза бежите.
9. Сергей Карташев (Elisy) 14.01.15 10:13
(8) zqzq,
(5) Elisy, 1С и сами планируют альтернативный конфигуратор на эклипс выпустить (к 1С 8.4?), вперёд паровоза бежите.

Наивно верить, что у 1С получится качественно сделать что-то новое?
10. Сергей Карташев (Elisy) 14.01.15 10:18
(7) Darklight,

Но, остаётся лишь вопрос эффективности получаемого кода. Насколько хорошо он будет работать. Такие конвертации из более мощного языка в менее мощный - всегда чреваты потерей эффективности конкретных алгоритмических реализаций (даже по сравнение с их аналогичной ручной реализацией на конечном языке). Эффективность и гибкость конвертации можно было бы увеличить, если бы сразу конвертировать в байт код машины 1С (что правильнее), как аналог IL-кода, платформы .NET.

Конвертация в код 1С - это компромисс, позволяющий отлаживать полученное решение средствами 1С. А так, конечно, оп-код был бы более предпочтительным. Принципиальных барьеров против этого нет.

Была бы интересна и обратная конвертация. Из кода 1С в код C# - для реализации обратного перевода проектов

По идее в полноценном проекте должна быть конвертация из 1С в C#, чтобы как можно больше разработчиков переманить на свою сторону.
11. Александр Топольский (AlexanderKai) 27.01.15 14:18
(8) zqzq,
Скорей всего получится, что "хотели как лучше, получилось как всегда".
Для написания сообщения необходимо авторизоваться
Прикрепить файл
Дополнительные параметры ответа