|
||||||||||
|
Программирование на Visual C++Выпуск №60 от 20 января 2002 г. Здравствуйте, дорогие подписчики! Рад снова вас приветствовать на страницах рассылки. Эх, меняются времена. Сейчас объем статей становится настолько большим, что они редко просто влезают в ограниченный 60 килобайтами выпуск без разбивания на две части. А когда все-таки влезают, то ни для чего другого места больше не остается ;-) Но я думаю, хорошие статьи все-таки для читателей важнее. Сегодня я предлагаю вам познакомиться с очередной концепцией новой платформы .NET – т.н. сборками. СТАТЬЯНемного о сборках Вводные положения При проектировании платформы .NET одной из задач являлось легкое развёртывание (инсталляция) и поддержка приложений, так как в настоящее время эта проблема стала серьезно беспокоить не только разработчиков, но и рядовых пользователей. Наверное, каждый знаком с ситуацией, когда после установки новой программы некоторые старые приложения наотрез отказывались работать. Ниже я вам поведаю о том, какое решение данной проблемы предоставила Microsoft на этот раз. Немного историиВсе началось очень давно, когда Microsoft еще только задумывалась над идеей повторного использования кода. В те времена для решения данной проблемы были созданы динамически подгружаемые библиотеки (Dynamic-Link Load Library, DLL). Они позволяли "выносить" часто используемый код в отдельные библиотеки, которые могли использовать любые приложения. Проблема была в том, что DLL изначально не предоставляли никаких средств управлениям версиями, тогда об этом просто никто не задумывался. Впоследствии Microsoft ввела Version Info (информацию о версии), которая помещалась в DLL как ресурс. Но это не решало проблему полностью, а лишь позволяло определять версию библиотеки, то есть отчасти гарантировало, что будет использоваться именно нужная библиотека. И никак не разъясняло вопрос о том, что делать, если версия библиотеки не совпадает с требуемой. К тому же, вся поддержка версий ложилась на плечи программиста. Для того чтобы корректно поддерживать старые версии, приходилось в уже новых библиотеках оставлять все старые функции, которые и поныне используются старыми приложениями. К примеру, в так называемом ядре Windows – библиотеке Kernel32.dll (которая, по сути, ядром-то и не является), присутствуют многие устаревшие функции, которые в настоящее время используются только устаревшими приложениями, но оставлены для совместимости (WinExec и др.).
Из-за таких функций в последствии сильно затрудняется разработка, так как их количество растет, подобно снежному кому, от версии к версии. Причём не все разработчики добросовестно поддерживают старые версии, чего, кстати, нельзя сказать о Microsoft. Из-за этого и возникают проблемы, похожие на ту, что описана в начале статьи. Представьте, что старая библиотека будет заменена новой, пусть даже и лучшей, но не поддерживающей старые функции. Старое приложение, рассчитанное на старую версию библиотеки, не найдя нужных ему функций, попросту завершит свою работу (собственно и не начиная её). Вторым шагом было создание COM, который уже предусматривал поддержу версий как неотъемлемую свою часть. Вся поддержка версий в COM в основном основывалась на регистрации приложений и компонентов COM в системном реестре. Что, как позже выяснилось, было далеко не гениальным решением. Поддержка совместимости версий все еще была возложена на плечи разработчика, что само по себе не подразумевало решения проблемы. Ведь что взбредет в голову простому разработчику – одному господу богу известно. Захочет он поддерживать старые версии, или нет, – это его личный выбор. От которого, кстати, будут очень зависеть пользователи. Кроме того, все эти записи о версиях в реестре оказались достаточно неэффективными, так как на практике не были защищены от внешних воздействий (скажем, от установки нового приложения). Порой эти записи повреждались, что было фатальным, и приходилось заново переустанавливать приложения, так и не зная реальной причины их краха. При проектировании .NET была поставлена задача разработать технологию, которая позволила бы решить проблему версий, быстрого развёртывания и изоляции приложений. В основу новой технологии легли сборки (Assembly), которые призваны решить обозначенные выше проблемы. Что же это такое – сборки (Assembly)?• Сборки – это наименьшие строительные блоки, на которых базируется платформа .NET. • Различия в версиях могут существовать только на уровне сборок; предполагается, что внутри сборки никакие элементы (классы, интерфейсы и т. п.) не могут иметь собственные версии. • Сборки являются хранилищами как для кода, так и для ресурсов. • Сборки самоописываемы – они содержат метаданные (metadata), которые несут в себе информацию о версии, зависимостях, типах, атрибутах и многое другое. • Сборки защищены – система защиты исполняемого кода использует права запуска индивидуально для каждой сборки. Автором сборки в метаданных записываются права на использование данной сборки кем бы то ни было, что позволяет защищать код "родными" для системы методами, не прибегая к продуктам сторонних производителей. Начнем с манифестаМанифест – это метаданные, включающие информацию о сборке, а именно: • Данные о версии – версию, имя и необязательные данные. • Список файлов – имена файлов, составляющих сборку, а также их контрольные суммы, вычисляющиеся при помощи криптографических хэш-функций во время создания сборки. Во время выполнения данные файлы проверяются по контрольным суммам, чтобы удостоверится в целостности данного файла, а так же в том, что файл не был подменён другим с таким же именем или просто его новой версией. • Зависимости от других сборок – имена и версии сборок, которые используются данной сборкой. Во время выполнения версии сборок строго сверяются, чтобы удостовериться в том, что загружена именно нужная сборка. • Экспортируемые типы и ресурсы. Видимость для этих объектов может быть двух типов: только для моей сборки (internal) и для всех (public), включая внешние запросы. • Свойства защиты. Здесь можно выделить три типа: • Права на запуск данной сборки. • Некоторые возможности сборки будут недоступны, если она не лицензирована. • Сборка должна запускаться только в том случае, если она лицензирована. Настало время "поработать руками" Для начала проверьте, правильно ли у вас настроены пути к Visual Studio.Net. Чтобы правильно настроить пути, вам всего лишь необходимо вызывать при загрузке (ну или как вам нравиться) файл vsvars32.bat, который расположен в директории …Microsoft Visual Studio.NET\Common7\Tools\. Давайте взглянем на пример который впоследствии нам предстоит скомпилировать и изучать. • Visual Basic.NET 'File: Some.vb 'Author: Copyright (C) 2001 Dubovcev Aleksey Imports System Public Class App Public Shared Sub Main() Console.WriteLine("Hello World") End Sub End Class • C# /* File: Some.cs Author: Copyright (C) 2001 Dubovcev Aleksey */ using System; public class Application { public static void Main() { Console.WriteLine("Hello World"); } } • Managed Visual C++ /* File: Some.cpp Author: Copyright (C) 2001 Dubovcev Aleksey */ #using <mscorlib.dll> using namespace System; void main() { Console::WriteLine("Hello World"); } Теперь, когда вы построили exe файл, запускайте утилиту ildasm.exe (Intermediate Language Disassembler – дизассемблер промежуточного языка) следующим образом: ildasm.exe /adv HelloWorld.exe Параметр командной строки /adv откроет дополнительные пункты меню, которые понадобятся нам позднее. Полную информацию о данной утилите вы сможете найти в .NET Framework Sdk. Рис. 1 Вы должны увидеть то же самое, что и на рисунке. Древовидная структура (далее просто дерево) показывает вам сборку изнутри. […]
Поэкспериментируйте немного с ildasm, чтобы привыкнуть к этой программе. Не пугайтесь при виде каких ни будь непонятных данных, дальше будет еще страшнее. :) Теперь откройте манифест (manifest) и внимательно посмотрите. Ниже я привожу содержание манифеста, полученное мной при помощи утилиты ildasm. // Microsoft (R) .NET Framework IL Disassembler. Version 1.0.2914.16 // Copyright (C) Microsoft Corp. 1998-2001. All rights reserved. // VTableFixup Directory: // No data. //Это ссылка на основную библиотеку классов .NET .assembly extern mscorlib { //Это хеш публичного ключа данной сборки //он нужен для подтверждения валидности сборки .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4.. //Версия сборки которая использовалась при создании приложения .ver 1:0:2411:0 } //Описание нашей сборки .assembly Some { // – The following custom attribute is added automatically, do not uncomment – // – Следующий атрибут добавлен автоматически, не убирайте комментарий // .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool, // bool) = ( 01 00 00 01 00 00 ) //Алгоритм по которому считается хэш .hash algorithm 0x00008004 //Версия нашей сборки .ver 0:0:0:0 } //Название запускаемого файла .module Some.exe // MVID: {2FA89A98-AD9F-4E31-8DB1-AB1FFB64A4F4} //Предпочтительный адрес для загрузки сборки .imagebase 0x00400000 //Подсистема (консоль, оконное приложение, приложение времени загрузки) .subsystem 0x00000003 //Выравнивание секций .file alignment 512 //Зарезервированный флаг .corflags 0x00000001 Что, вам кажется, что это полная чушь? Ошибаетесь, если в этом разобраться, что, кстати, не так уж и трудно, то вам откроется много очень полезной и порой необходимой информации. В начале вы увидите записи со словами .assembly extern, которые описывают зависимости от внешних сборок, необходимых для функционирования этой программы. А данные, идущие далее в блоке, заключённом в фигурных скобках, описывают версию и контрольную сумму сборки. Эти данные берутся из сборок при компиляции программы, что гарантирует использование именно тех сборок, которые использовались при компиляции и тестировании. Далее следует .assembly, но уже без модификатора extern. С этой директивы и начинается описание нашей с вами сборки. Как вы могли догадаться, .ver описывает версию нашей сборки. Ну а .hash algorithm определяет функцию, по которой будет вычисляться хэш, но об этом я расскажу позднее. Затем идут описания имени самого модуля, подсистемы исполнения, информация о выравнивании секций и еще некоторые данные. Полная документация по этому вопросу находится в Framework SDK. Более подробно об устройстве манифеста я расскажу далее. Давайте "копнём" поглубже Как же сборка устроена изнутри? Что у нее "под капотом"? Оказывается, не так все и страшно, как вам могло показаться. Сборка помещается внутри файла в формате PE (Portable Executable), то есть внутри DLL или EXE. Здесь все зависит от того, будет ли сборка самостоятельной программой или "библиотекой". Любая сборка импортирует функции из библиотеки mscoree.dll, которая является частью среды исполнения. Исполняемые файлы (EXE) импортируют из этой библиотеки функцию _CorExeMain, которую они вызывают для своего запуска. А происходит это так: как и в любом exe-файле, в нашем присутствует точка входа - это маленькая функция (6 байт), которая призвана передавать управление функции _CorExeMain из библиотеки mscoree.dll. Когда данная функция получает управление, она находит в exe-файле свою точку входа и начинает выполнение с нее. Вы можете проверить все сказанное мною сами при помощи утилиты dumpbin, запустив ее с параметрам /imports. Правда, у файла, скомпилированного на Managed C++, вы можете увидеть много других импортов. Не пугайтесь, это нормально, так как MC++ одновременно поддерживает как управляемые, так и неуправляемые данные (managed/unmanaged data). А значит, может делать самостоятельные системные вызовы в обход CLR. Динамически загружаемые библиотеки импортируют функцию _CorDllMain, которую они вызывают из DllMain, "точки входа" DLL. Сама же функция точки входа ничего, кроме вызова _CorDllMain, не делает. С exe-сборками дело обстоит аналогичным образом, только вместо _CorDllMain, импортируется функция _CorExeMain. Вот как выглядят точки входа в dll– в exe-сборки.
В будущих версиях Windows Microsoft планирует изменить процесс загрузки, при котором реальная точка входа будет находиться не таким странноватым способом. Вы спросите: а почему нельзя было сделать сразу, нормально? Это на самом деле не такой простой вопрос, как кажется. Ведь новые исполняемые файлы, по сути дела, со старыми ничего общего не имеют, а надо сделать так, чтобы они могли запускаться существующими версиями Windows без изменения загрузчика файлов. Microsoft пошла по давно известному пути "оборачивание нового в старое", то есть сам exe-файл является оберткой, для реального исполняемого файла CLR. Благодаря этому подходу файлы могут запускаться на уже существующих версиях Windows без изменения их самих. Сборка может состоять из следующих частей: манифест – метаданные, описывающие сборку, описание типов, код в виде промежуточного языка (IL) и набор ресурсов. Не все из них должны обязательно присутствовать в каждой сборке, кроме, разумеется, манифеста. Сборка также может быть разбита на несколько файлов. Для большей наглядности я нарисовал, как это все происходит. Рис. 2 Как вы можете видеть, за пределами сборки можно помещать не только другие специальные файлы, которые тоже являются сборками, а любые другие файлы, какие вам захочется, к примеру, картинки. Это дает несколько преимуществ. Во-первых, вы можете обновлять отдельные файлы сборки, не прибегая к компиляции, во-вторых, как я уже говорил ранее, не имеет никакого значения, где размещены эти файлы. Внешние файлы подгружаются только по мере надобности, что позволяет оптимизировать сетевой трафик, если часть приложения находиться в сети. Тут даже можно немного схитрить: сделать основной сборку которая не несет в себе ничего, кроме зависимостей. Тогда вы сможете поставить вашу программу в малюсеньком файле, который, по мере надобности, скачает все, что ему нужно, из сети. Управлениями версиями и изоляция Немного лирикиПо умолчанию COM-компоненты общедоступны. Я имею в виду, что для них присутствуют записи в реестре, с которым любой может делать все, что захочет, а также то, что большинство приложений хранит свои библиотеки в системных директориях, хотя Microsoft и не советует этого делать. То есть ни о какой изолированности приложений говорить не приходилось. Потенциально, любое приложение может быть приведено в неработоспособное состояние при обстоятельствах, никак от него не зависящих. Концепция сборок предоставляет полную изолированность приложений (ну, почти полную). Сборки делятся на два типа: • приватные (private) – те которые используются только самим приложением. • совместные (shared) – те которые используются всеми. Приватные сборки поставляются с самим приложением, используются только им и храниться в его папке. Microsoft рекомендует использовать совместные сборки только при крайней необходимости. Да, вы, конечно, можете сказать, что это нецелесообразно – с каждым приложение поставлять одни и те же сборки, можно ведь сделать сборку совместной, сэкономив тем самым немного места. Но на самом деле теперь, когда цена за мегабайт дискового пространства неуклонно уменьшается, а объем дисков неуклонно увеличивается, проблемы объема, занимаемого вашим приложением, не должны вас беспокоить. Собственно, здесь нужно выбирать между устойчивостью и небольшим увеличением эффективности в виде сохранения дискового пространства. Я лично думаю, что надо остановиться на первом. Хотя эта проблема может показаться для вас незначительной, на самом деле все очень серьёзно. Вдумайтесь: над решением этой проблемы компания Microsoft работает с выхода первых версий Windows и по сей день. И только сейчас в .NET были предложены четко стандартизированные и хорошо продуманные средства для решения этой проблемы. Хотя, как знать? Все еще может обернуться провалом, как это бывало и раньше. Приватные сборкиПриватные сборки видны только самому приложению и никому более, то есть приложение изолируется от внешнего воздействия как других программ, так и самой операционной системы. Соответственно, приватные сборки лишены многих проблем, связанных с совместными сборками. К примеру, такой, как уникальность имен: так как сборка приватна, нет необходимости заботится об уникальности имен во всем глобальном пространстве имен. Концепция приватных сборок сильно упрощает развёртывание (инсталляцию) ваших приложений, так как больше не придется делать записей в реестре, подобных тем, которые вы делали ранее для регистрации ваших COM-компонентов. Теперь вы будете просто копировать ваши сборки в директорию вашего приложения или в подчиненные директории. Общая среда исполнения (CLR) при запуске вашего приложения прочитает его манифест и определит, какие сборки необходимы. Затем будет произведён процесс зондирования (probing) (звучит прямо как зомбирование :) директории вашего приложения на предмет нужной сборки, сборка соответственно определяется по имени файла, определенного в манифесте.
Среда исполнения может подходить к зондированию достаточно интеллектуально: к примеру, могут быть выбраны локализованные для данного региона сборки, если, конечно, они поставляются автором приложения. Как выбираются именно нужные сборки, я расскажу позднее. Как я говорил ранее, в манифесте сборки указывается строгая информация о версии. Но для приватных сборок точное соответствие версий не навязывается средой исполнения, так как считается, что разработчик имеет полный контроль над своим приложением и, соответственно, может сам управлять версиями, если это ему нужно. Совместные сборкиСреда исполнения .NET поддерживает также совместные сборки. Это сборки, которые могут быть использованы сразу несколькими приложениями и которые, соответственно, "видны" всем. Правда, к таким сборкам предъявляются более строгие правила, к приватным сборкам. Например, необходима уникальность имен сборки: имена внутри сборки не должны конфликтовать с уже существующими в глобальном пространстве имен, предоставляемом средой исполнения по умолчанию, хотя система и предоставляет сервисы защиты имен (protection of the name). Специально для реализации этого сервиса была разработана технология Shared Names (совместные имена), описываемая далее. Действия системы управления версиями при поиске необходимых сборок, могут быть изменены при помощи политики версий, которую сможет изменять администратор или приложения. Данная политика позволит принудительно изменить версию сборки, запрашиваемой приложением, а также поведение среды разработки при поиске и загрузке сборок. То есть приложение можно заставить использовать сборку другой версии, даже если оно на это не рассчитано. Данная политика настраивается при помощи файла конфигурации, который помещается в каталог приложения и имеет то же имя, что и у приложения, только с расширением .config. Подробно формат этих файлов я сейчас описывать не буду.
Совместные сборки хранятся в глобальном кеше сборок (global assembly cache – GAC). Сборки, хранящиеся там, используются многими приложениями. К ним также имеет доступ администратор, который при необходимости сможет ставить патчи (Udgrade) на совместно используемые сборки, которые, соответственно, окажут влияние на все приложения, которые используют данные сборки. Для примера это может быть какая-нибудь заплатка на общие библиотеки среды, к примеру исправление в каком-нибудь классе. Реально хранилище сборок располагается в директории WinNt\assembly. Если вы ее будете просматривать при помощи проводника, вы увидите что-нибудь похожее на приведённый ниже рисунок. Рис. 3 Но на самом деле это не директория WinNt\Assembly. Когда вы открываете эту директорию, активизируется специальное расширение оболочки Windows и показывает вам сборки, которые сейчас хранятся в GAC. На самом же деле у хранилища, расположенного в этой директории, достаточно интересная структура. К примеру, у меня, она выглядит так: Рис. 4 Структура этих директорий хотя и может показаться с первого взгляда очевидной, но на самом деле она очень хорошо продумана и спроектирована. Как вы уже могли заметить, в папке GAC есть подпапки, представляющие каждую сборку. Можно было бы подумать, что в них и будут храниться файлы с самими сборками, ан нет. Разработчики GAC поступили куда дальновиднее, в каждой папке, представляющей сборку хранятся подпапки, разбивающие данную сборку по версиям. Таким образом, у нас может храниться любое количество версий одной и той же сборки. Зачем же, спрашивается, это нужно? Ведь такой подход никак ни способствует экономии места на диске. Все на самом деле проще, чем вы могли подумать. Как я уже говорил ранее, Microsoft стремилась сделать приложения как можно более стабильными и даже пошла ради этого на жертву в виде вашего дискового пространства. При таком подходе каждое приложение сможет использовать именно ту совместную сборку, на которую оно рассчитано. То есть при загрузке приложения для него будет выбираться сборка именно той версии, которую он запросит, хотя может существовать и гораздо более новая версия этой же сборки.
Поиск в глобальном хранилище сборок идет, основываясь на строгих сведениях о версии, что позволяет избегать многих проблем, описанных ранее. Для повышения надежности и стойкости системы GAC, вы не сможете производить какие либо действия (кроме, конечно, простого просмотра) с глобальным хранилищем, если вы не будете иметь прав администратора. Такая политика введена по умолчанию в отношении GAC, хотя при желании ее можно будет изменить. А что это вообще означает? Тут, на самом деле, все интереснее, чем могло бы показаться с первого взгляда. Смысл данной политики таков: пользователь, не имеющий прав администратора, не сможет как-либо повлиять на работу приложений, установленных в системе, хотя и сможет устанавливать приложения, но лишь те, которые не будут использовать совместных сборок. Иными словами, ничего глобального простому пользователю испортить не удастся. Как "работает" информация о версияхСама версия состоит из четырёх чисел; для наглядности, пожалуй, даже нарисую. Рис. 5 На практике это выглядит, к примеру, так: 1.0.2.3. Где: • Major – основная версия. • Minor – подверсия приложения. • Build – количество построений (полных компиляций) для данной версии. • Revision – Номер ревизии для текущего построения. В версии выделяется основная часть и дополнительная. При поиске нужной сборки, основная часть версии должна строго совпадать, а с дополнительной частью всё происходит хитрее. Если будет найдено несколько сборок с одинаковыми основными частями, то будет выбрана сборка с наибольшей дополнительной частью. Версию сборки вы можете задать при помощи параметра командной строки либо при помощи атрибута System.Reflection.AssemblyVersionAttribute. Здесь, правда, есть одна хитрость: вы можете задавать версию не полностью, а только ее часть. К примеру вот так: "1.*","1.5.*,@1.5.2.*". При отсутствии каких либо частей, компилятор допишет их сам по следующим правилам: • Minor – приравнивается к нулю • Build – приравнивается количеству дней прошедших с первого января 2000 года • Revision – приравнивается количеству секунд, прошедших с полуночи, деленных на два. Такая, казалось бы, странная схемы выбрана далеко не случайно: она позволяет гарантировать уникальность версии для каждого построения приложения, что, вообщем-то, немаловажно. Технологи прямого запуска (Side-By-Side Execution)Я долго думал, как на словах описать эту технологию, но что-то ничего не лезло в голову, поэтому я решил нарисовать. Думаю, что, посмотрев на рисунок, вы сразу многое поймёте. Главное, обратите внимание на версии. Рис. 6 Идея состоит в том, что для одного и того же приложения могут быть загружены разные версии одной и той же сборки, и при этом оно ничего не будет знать об этом. Для примера, библиотеки, обозначенные на рисунке цифрами 1 и 2, будут загружены для нашего приложения и может даже будут одновременно работать, но ни одна из них не узнает о существовании другой, что позволит избежать каких-либо конфликтов. Проверка подлинности Допустим, что среда выполнения (CLR) при загрузке приложения нашла требуемую сборку с подходящей версией. По идее, надо начать ее загружать, но возникает вопрос: а как узнать, что это действительно именно та сборка, которая нам нужна, а не другая. Ведь возможно, что кто-то захочет выдать свою сборку за чужую, или нужная сборка оказалась поврежденной, скажем, при передачи по сети. COM, к примеру, не предусматривал решения этой проблемы. В .NET для решения данного вопроса используются односторонние хэш-функции, в простонародье называемые контрольными суммами. В дополнении к информации об используемой сборке, во время компиляции считается контрольная сумма используемой сборки и помещается вместе с информацией о версии. Во время загрузки проверяется контрольная сумма, и на основание этой проверки будет вынесет вердикт о валидности сборки. По умолчанию используется хэш-функция SHA1. Плюс еще используется технология подписывания (signing) сборок, основанная на открытых криптографических алгоритмах с парными ключами. Вообще, ребята из Microsoft здорово поработали над этой проблемой. Ими было применено очень много интересных решений, о которых я, возможно, расскажу в отдельной статье. Развертывание приложенийДля простого развертывания приложений были существенно расширены сервисы, предоставляемые Windows Installer. Теперь он полностью поддерживает .NET. Создание инсталляций станет более простым, так как теперь данная возможность интегрирована в стандартную поставку среды разработки .NET. То есть вы сможете использовать все современные сервисы, предоставляемые Windows Installer, в ваших приложениях, не прибегая к средствам сторонних производителей. А предоставляет он их, кстати, не мало, но об этом в отдельной статье. ЗаключениеХотя вам может показаться, что все, что я рассказал, не особо нужно, но на самом деле это совсем не так. Вы, конечно, можете сказать: "Зачем мне знать какие-то страшные низкоуровневые подробности, когда можно заняться чем-нибудь конкретным, к примеру написать компонент для .NET". Я считаю, что такой подход крайне неверен. Изучая систему сверху, невозможно понять всей сути происходящего в ней, вы сможете лишь заучить некоторые стандартные приемы работы с системой, о которых вы где-либо прочитаете или найдете соответствующий пример. Если же понять систему изнутри, то она откроется для вас с совершенно новой стороны. Вы сможете решать проблемы совершенно неординарным способом, основываясь не на каких-то там примерах, а на фундаментальных знаниях о системе. То, что я осветил в статье, является лишь маленькой верхушкой огромного айсберга .NET. Впоследствии я буду описывать некоторые из технологий, упомянутых выше, более детально, но все равно обо всем написать я никак не смогу при всем своем желании. Прочитав статью, поэкспериментируйте, посмотрите, поизучайте то, что вас заинтересует. Главное – никогда не останавливайтесь на достигнутом. Это все на сегодня. До скорого! (Алекс Jenter jenter@rsdn.ru) (Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN. ) |
|
||||||||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||||||||
|