Главная страница
    Top.Mail.Ru    Яндекс.Метрика
Форум: "WinAPI";
Текущий архив: 2013.03.22;
Скачать: [xml.tar.bz2];

Вниз

Внедрение кода в другой поток. Замечания, дополнения, критика   Найти похожие ветки 

 
SPeller ©   (2009-11-24 02:40) [0]

Есть задача внедряться в произвольный поток своего процесса, выполнять некий код, возвращать на круги своя. Смотрел на модуль AsyncCalls, но там используется VCL, что в моей задаче недостаточно, ибо целевой поток может быть чей угодно, помимо VCL. Но понравился организованный там принцип кодирования внедрения в основной поток через Enter/LeaveMainThread, поэтому взял его за основу. В основном придется внедряться в главный GUI поток чтобы создать там окно со своей оконной процедурой и в дальнейшем слать ему сообщения для выполнения кода в контексте основного потока не нарушая нормальное течение выполнения. Задачу вроде решил, вроде даже всё работает, но что-то какие-то сомнения у меня, всё ли правильно сделал, ибо решал задачу 2 дня, но окончательное решение родилось как-то быстро и получилось (на мой взгляд) простым. Но на сколько оно правильно идеологически - вопрос пока открытый. Вот код, писал на Д2009, должно работать на Д2007:

unit Synchronizer;

interface
uses
 Windows, Classes, SysUtils, libWinApi, Messages, SyncObjs;

type

 PSyncRec = ^TSyncRec;
 TSyncRec = record
   idCaller: Cardinal;
   idTarget: Cardinal;
   hCaller: THandle;
   hTarget: THandle;
   ctxTarget: TContext;
   Lock: TRTLCriticalSection;
   RefCount: Integer;
   function AddRef: Integer;
   function Release: Integer;
 end;

 TSynchronizer = class(TThread)
 private
   FSyncList: TList;
   FSyncListLock: TCriticalSection;
 protected
   function CreateSync(idTarget: Cardinal): PSyncRec;
   procedure BeginSync(SR: PSyncRec);
   function FindSync(idTarget: Cardinal): PSyncRec;
   procedure EndSync(SR: PSyncRec);
   procedure FreeSync(SR: PSyncRec);
   procedure DoSync(SR: PSyncRec; Flag: PCardinal);
   procedure DoSyncEnd(idTarget: Cardinal; Flag: PCardinal);
   function LockTargetThreadSync(idTarget: Cardinal): PSyncRec;
   procedure UnlockTargetThreadSync(idTarget: Cardinal);
   function ProcessMessages: Boolean;
   function ProcessMessage(var Msg: TMsg): Boolean;
   procedure Execute; override;
   procedure Quit;
   procedure SyncListLock;
   procedure SyncListUnlock;
 public
   constructor Create;
   destructor Destroy; override;

   function Sync(TargetThreadID: Cardinal): Boolean;
   procedure SyncEnd;
   procedure Post(Msg, Param1, Param2: Integer);
 end;
 

function InjectThread(TargetThreadID: Cardinal): Boolean;
procedure LeaveThread;
 

implementation

const
 WM_MYSYNC = WM_USER + 1;
 WM_MYSYNCEND = WM_USER + 2;

var
 Sync: TSynchronizer;

function InjectThread(TargetThreadID: Cardinal): Boolean;
begin
 if (TargetThreadID <> GetCurrentThreadId) then
   Result := Sync.Sync(TargetThreadID)
 else
   Result := True;
end;

procedure LeaveThread;
begin
 Sync.SyncEnd;
end;
 
 
{ TSynchronizer }

procedure TSynchronizer.BeginSync(SR: PSyncRec);
begin
 SyncListLock;
 try
   SR.hCaller := OpenThread(THREAD_ALL_ACCESS, False, SR.idCaller);
   SR.hTarget := OpenThread(THREAD_ALL_ACCESS, False, SR.idTarget);
 finally
   SyncListUnlock;
 end;
end;

constructor TSynchronizer.Create;
begin
 inherited Create(True);
 FSyncList := TList.Create;
 FSyncListLock := TCriticalSection.Create;
 Resume;
end;

function TSynchronizer.CreateSync(idTarget: Cardinal): PSyncRec;
begin
 SyncListLock;
 try
   Result := AllocMem(SizeOf(Result^));
   Result.idTarget := idTarget;
   Result.Lock.Initialize;
   FSyncList.Add(Result);
 finally
   SyncListUnlock;
 end;
end;

destructor TSynchronizer.Destroy;
begin
 SyncListLock;
 SyncListUnlock;
 FSyncListLock.Free;
 FSyncList.Free;
 inherited;
end;

procedure TSynchronizer.DoSync(SR: PSyncRec; Flag: PCardinal);
var
 ctxCaller: TContext;
begin
 BeginSync(SR);
 SuspendThread(SR.hCaller);
 SuspendThread(SR.hTarget);
 SR.ctxTarget.ContextFlags := CONTEXT_FULL;
 GetThreadContext(SR.hTarget, SR.ctxTarget);
 ctxCaller.ContextFlags := CONTEXT_FULL;
 GetThreadContext(SR.hCaller, ctxCaller);

 SetThreadContext(SR.hTarget, ctxCaller);
 Flag^ := 1;
 PostThreadMessage(SR.idTarget, WM_USER, 0, 0);
 ResumeThread(SR.hTarget);
end;

procedure TSynchronizer.DoSyncEnd(idTarget: Cardinal; Flag: PCardinal);
var
 sr: PSyncRec;
 ctxTmp: TContext;
begin
 sr := FindSync(idTarget);
 SuspendThread(sr.hTarget);
 ctxTmp.ContextFlags := CONTEXT_FULL;
 GetThreadContext(sr.hTarget, ctxTmp);
 SetThreadContext(sr.hTarget, sr.ctxTarget);
 SetThreadContext(sr.hCaller, ctxTmp);
 Flag^ := 1;
 ResumeThread(sr.hTarget);
 ResumeThread(sr.hCaller);
 EndSync(sr);
 UnlockTargetThreadSync(sr.idTarget);
end;

procedure TSynchronizer.EndSync(SR: PSyncRec);
begin
 SyncListLock;
 try
   CloseHandle(SR.hCaller);
   CloseHandle(SR.hTarget);
   SR.hCaller := 0;
   SR.hTarget := 0;
   FillChar(SR.ctxTarget, SizeOf(SR.ctxTarget), 0);
   SR.idCaller := 0;
 finally
   SyncListUnlock;
 end;
end;

procedure TSynchronizer.Execute;
var
 msg: TMsg;
begin
 PeekMessage(msg, 0, WM_USER, WM_USER, PM_NOREMOVE);
 while ProcessMessages and WaitMessage do;
end;

function TSynchronizer.FindSync(idTarget: Cardinal): PSyncRec;
var
 i: Integer;
begin
 SyncListLock;
 try
   for i := 0 to FSyncList.Count - 1 do
   begin
     Result := FSyncList[i];
     if (Result.idTarget = idTarget) then
       Exit;
   end;
 finally
   SyncListUnlock;
 end;
 Result := nil;
end;

procedure TSynchronizer.FreeSync(SR: PSyncRec);
begin
 SyncListLock;
 try
   FSyncList.Remove(SR);
   SR.Lock.Destroy;
   FreeMem(SR);
 finally
   SyncListUnlock;
 end;
end;

function TSynchronizer.LockTargetThreadSync(idTarget: Cardinal): PSyncRec;
begin
 SyncListLock;
 try
   Result := FindSync(idTarget);
   if (Result = nil) then
     Result := CreateSync(idTarget);
   Result.AddRef;
 finally
   SyncListUnlock;
 end;
 Result.Lock.Enter;
end;

procedure TSynchronizer.Post(Msg, Param1, Param2: Integer);
begin
 PostThreadMessage(ThreadID, Msg, Param1, Param2);
end;

function TSynchronizer.ProcessMessage(var Msg: TMsg): Boolean;
begin
 Result := True;
 case Msg.message of
   WM_MYSYNC:
     DoSync(Pointer(Msg.lParam), Pointer(Msg.wParam));
   WM_MYSYNCEND:
     DoSyncEnd(Msg.lParam, Pointer(Msg.wParam));
   WM_QUIT:
     Result := False;
 end;
end;

function TSynchronizer.ProcessMessages: Boolean;
var
 item: TMsg;
begin
 Result := True;
 while Result and PeekMessage(item, 0, 0, 0, PM_REMOVE) do
   Result := ProcessMessage(item);
end;

...


 
SPeller ©   (2009-11-24 02:41) [1]

procedure TSynchronizer.Quit;
begin
 Post(WM_QUIT, 0, 0);
 WaitFor;
end;

function TSynchronizer.Sync(TargetThreadID: Cardinal): Boolean;
var
 flag: Cardinal;
 sr: PSyncRec;
begin
 Result := True;
 sr := LockTargetThreadSync(TargetThreadID);
 sr.idCaller := GetCurrentThreadId;
 flag := 0;
 Post(WM_MYSYNC, Integer(@flag), Integer(sr));
 while (flag = 0) do;
 if (sr <> nil) then;
end;

procedure TSynchronizer.SyncEnd;
var
 flag: Cardinal;
begin
 flag := 0;
 Post(WM_MYSYNCEND, Integer(@flag), GetCurrentThreadId);
 while (flag = 0) do;
end;

procedure TSynchronizer.SyncListLock;
begin
 FSyncListLock.Enter;
end;

procedure TSynchronizer.SyncListUnlock;
begin
 FSyncListLock.Leave;
end;

procedure TSynchronizer.UnlockTargetThreadSync(idTarget: Cardinal);
var
 sr: PSyncRec;
begin
 SyncListLock;
 try
   sr := FindSync(idTarget);
   sr.Lock.Leave;
   if (sr.Release = 0) then
     FreeSync(sr);
 finally
   SyncListUnlock;
 end;
end;

{ TSyncRec }

function TSyncRec.AddRef: Integer;
begin
 Result := InterlockedIncrement(RefCount);
end;

function TSyncRec.Release: Integer;
begin
 Result := InterlockedDecrement(RefCount);
end;

initialization
 Sync := TSynchronizer.Create;

finalization
 Sync.Quit;
 Sync.Free;

end.


Использовать так:

 TMyThread = class(TThread)
 protected
   procedure Execute; override;
 end;

{ TMyThread }

procedure TMyThread.Execute;
begin
 InjectThread(MainThreadID);
 try
   if (GetCurrentThreadId = MainThreadID) then
   begin
     MessageBox(Form1.Handle, "Work!!!", nil, 0);
     //Logger.LogText("", "Injected code");
   end;
 finally
   LeaveThread;
 end;
end;

{ TForm1 }

procedure TForm1.Button3Click(Sender: TObject);
var
 t: TMyThread;
begin
 t := TMyThread.Create(True);
 t.FreeOnTerminate := True;
 t.Resume;
end;


Из модуля libWinApi понадобится следующее:

const
 THREAD_ALL_ACCESS                 = $1F03FF;

function OpenThread(
 dwDesiredAccess: DWORD; bInheritHandle: BOOL; dwThreadId: DWORD): THandle; stdcall;
function OpenThread; external kernel32 name "OpenThread";


Принцип работы. Есть поток-менеджер, который обрабатывает запросы на синхронизацию потоков. Вызывающий поток, желающий выполнить часть своего кода от имени другого потока, заходит в критическую секцию целевого потока (чтобы исключить одновременные внедрения в один поток), отправляет запрос менеджеру и зависает в бесконечном цикле в ожидании когда менеджер обработает запрос и выпустит из цикла. Менеджер, получив запрос на синхронизацию, замораживает оба потока, и вызывающего и целевого, затем через Get/SetThread Context задает целевому потоку контекст вызывающего и отпускает целевой поток на выполнение. Для случая когда целевой поток находится в ожидании сообщения менеджер шлет ему сообщение чтобы оживить поток, иначе всё висит до прихода потоку любого сообщения. Таким образом код вызывающего потока со стеком и прочим продолжает выполнение под управлением целевого потока. Дойдя до конечной точки поток снова стучится к менеджеру с запросом на возвращение на круги своя. Тот останавливает целевой поток, возвращает ему его оригинальный контекст, а вызвавшему потоку вручает контекст, который получился в результате выполнения его кода целевым потоком, и выходит из критической секции, разрешая другим потокам внедряться. Вот и всё.

Помимо возможных подводных камней думаю еще о целесообразности юзать свою очередь вместо системной очереди сообщений. Насколько она выгоднее системной в плане быстродействия?

Еще один глюк - у меня сейчас из критической секции выходит другой поток, а не тот, который ее занял. Вот и думаю, на сколько это опасно? Ведь работает вроде. Перенос выхода из критической секции в конец метода SyncEnd выдал плавающий экзепшин...

Нужны ваши мысли, замечания, критика


 
Leonid Troyanovsky ©   (2009-11-24 07:58) [2]


> SPeller ©   (24.11.09 02:40)  

> поэтому взял его за основу. В основном придется внедряться
> в главный GUI поток чтобы создать там окно со своей оконной
> процедурой и в дальнейшем слать ему сообщения для выполнения

Тоже мне бином Ньютона.

>    SR.hCaller := OpenThread(THREAD_ALL_ACCESS, False, SR.
> idCaller);
>    SR.hTarget := OpenThread(THREAD_ALL_ACCESS, False, SR.
> idTarget);

??

> procedure TSynchronizer.DoSync(SR: PSyncRec; Flag: PCardinal);

Ой, как все запущено.

Так перректально работать с собственными потоками?

Код - в печь, почитать что-либо, скажем, хоть
http://rsdn.ru/summary/227.xml

--
Regards, LVT.


 
Вариант   (2009-11-24 08:01) [3]

Ответ на вопрос


> Еще один глюк - у меня сейчас из критической секции выходит
> другой поток, а не тот, который ее занял. Вот и думаю, на
> сколько это опасно? Ведь работает вроде.



Цитата из MSDN
> If a thread calls LeaveCriticalSection when it does not
> have ownership of the specified critical section object,
>  an error occurs that may cause another thread using EnterCriticalSection
> to wait indefinitely.


И приблизительный перевод
"если поток вызвавший LeaveCriticalSection, не является владельцем указанного объекта критическая секция, возникает ошибка , которая может привести к неопределенному по времени ожиданию другим потоком, вызвавшим EnterCriticalSection".


 
SPeller ©   (2009-11-24 08:21) [4]


> Ой, как все запущено

> Код - в печь, почитать что-либо, скажем, хоть
> http://rsdn.ru/summary/227.xml

Что именно там читать? Можно конкретней что не так?


> Вариант   (24.11.09 08:01) [3]
Всё понял


 
Leonid Troyanovsky ©   (2009-11-24 08:33) [5]


> SPeller ©   (24.11.09 08:21) [4]

> > http://rsdn.ru/summary/227.xml

> Что именно там читать?

Что понравится, то и читай.
Там и рядом еще много всякой мудрости.

> Можно конкретней что не так?

Все не так.
Не работают со своими потоками через функции отладки.

Да и, во-ще, смешано все в кучу: гуи, контексты, критсекции
и оконные сообщения.

Пока неясна даже цель (или смысл) показанного.

--
Regards, LVT.


 
SPeller ©   (2009-11-24 08:46) [6]


> Что понравится, то и читай.
> Там и рядом еще много всякой мудрости
Не нашел ничего, что мне нужно. Потому и спросил, что же там для меня полезного?


> Все не так.
> Не работают со своими потоками через функции отладки
Как так? Работают, отчего нет? Вроде цель с огрехами, но достигнута.


> Да и, во-ще, смешано все в кучу: гуи, контексты, критсекции
> и оконные сообщения.
гуй - потому что часто придется залазить в поток, который обрабатывает гуй, контексты - ну это то, чем живет поток, на сколько я понимаю, критсекции - для предотвращения одновременного доступа к одному потоку, сообщения - только для организации очереди (я ведь еще и спросил не лучше ли будет системную очередь заменить своей). Может, прежде чем раздавать не очень полезную критику стоит хоть немного вникнуть в решение?


> Пока неясна даже цель (или смысл) показанного
Критика. Только не написал сразу что важна не "критика ради критики", а критика по существу. Пока что от тебя я увидел только непонятные нападки в духе "выкинуть всё и переписать" без объяснений. Потому что тебе так почудилось. Ты спроси что тебе не понятно в реализации - я тебе расскажу зачем я написал код именно так, а не иначе.


 
Leonid Troyanovsky ©   (2009-11-24 09:07) [7]


> SPeller ©   (24.11.09 08:46) [6]

> > Там и рядом еще много всякой мудрости
> Не нашел ничего, что мне нужно.

А мы пока не поняли, что тебе нужно.

> Как так? Работают, отчего нет? Вроде цель с огрехами, но
> достигнута.

Чего непонятного? Это debug API.
Работать так со _своими_ потоками - извращение.

> гуй - потому что часто придется залазить в поток, который
> обрабатывает гуй

Чего в него лазить? Послал оконное сообщение - синхронное
или асинхронное, все и выполнит. И очереди, слава богу, есть.

> контексты - ну это то, чем живет поток, на сколько я понимаю

Пока не понимаем, что ты понимаешь.

Пока не ясно само устремление выполнять код в контексте
чужого потока. Для гуи я еще могу понять, но все уже
украдено до нас, см. выше.
Скорее всего, на это толкает некий существующий
непотокобезопасный (видимо, кривонаписанный) код.
Ну, если уж так хочется в чужом изволь - см., например,
QueueUserAPC и прочий  LPC.

> вникнуть в решение?

Решение чего? Проблема не озвучена.

> нападки в духе "выкинуть всё и переписать" без объяснений

Для объяснений нужна база. Пока я видел одну путанницу.

--
Regards, LVT.


 
SPeller ©   (2009-11-24 10:34) [8]


> QueueUserAPC
А оно будет работать когда поток в ожидании оконного сообщения и чужим программистом не было предусмотрено что кто-то захочет залезть в поток? Когда поток в введен в ожидание не функциями SleepEx, SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx, or MsgWaitForMultipleObjectsEx, как хочет того msdn?


> А мы пока не поняли, что тебе нужно.
Нужно единожды создать окно в целевом потоке чтобы затем некий код выполнять синхронизированно с этим потоком. Поток, в который надо влезть, неизвестного происхождения и может быть написан кем угодно, поскольку мой код может быть как скомпилен с приложением вместе, так и быть в длл, в том числе динамически загружаемой (в этом случае поток, инициализирующий длл, не обязан быть главным потоком приложения, поэтому MainThreadID, доступный в ртл длл, будет указывать не на то что нужно).


> Это debug API.
> Работать так со _своими_ потоками - извращение
Ну и что что debug. Свои обязанности выполняет ведь. Или теперь недокументированными функциями тоже нельзя пользоваться?


 
SPeller ©   (2009-11-24 10:49) [9]


> Скорее всего, на это толкает некий существующий
> непотокобезопасный (видимо, кривонаписанный) код
Прорицатель, да? :) Предсказамус :) То, что я могу чего-то не знать, еще не говорит что я полный ламер подобно большинству вопрошающих, и меня надо пинать в привычном местном мастаковском духе. Благо у меня иммунитет уже к этому :) Ах да, простите, когда думал как решить задачу на глаза ничего кроме Get/SetThreadCOntext не попалось, и здесь я, простите, не спрашивал как решить. Что нашел, что придумал то и выложил показать и спросить.

Толкает на это всё то, что создается объект, который, подобно COM объекту в апартменте, должен выполняться в главном потоке с сериализацией вызовов. Некий рабочий поток может через мой ком-подобный объект обращаться к гую. Вот тут и нужно чтобы мой объект был в одном потоке с этим гуем. При этом абсолютно не обременяя гуйный поток заботой об этом.


 
SPeller ©   (2009-11-24 12:08) [10]

Кстати, по поводу debug некашерно юзать в программа. Интересно, для чего в мсдн в описании QueueUserAPC написано:

hThread
[in] Handle to the thread. The handle must have the THREAD_SET_CONTEXT access right


Уж не для "страшных" ли debug функций Get/SetThreadContext, для которых всё тот же мсдн говорит The thread identified by the hThread parameter is typically being debugged, but the function can also operate when the thread is not being debugged?


 
Leonid Troyanovsky ©   (2009-11-24 12:33) [11]


> SPeller ©   (24.11.09 10:49) [9]

> чтобы мой объект был в одном потоке с этим гуем. При этом
> абсолютно не обременяя гуйный поток заботой об этом.

Для создания в произвольном GUI thread окна много усилий не требуется.
Для синхронного вызова любых процедур (функций), выполняющихся
в контексте оного потока, достаточно SendMessage.

Истязательство других потоков пока ничем оправдано.

--
Regards, LVT.


 
Leonid Troyanovsky ©   (2009-11-24 12:43) [12]


> SPeller ©   (24.11.09 10:49) [9]

>  Что нашел, что придумал то и выложил показать и спросить.

Ну, я, например, и ответил. Глумлений же, IMO, пока не допускал.
Предположение о кривости некого кода с моей стороны вполне
обосновано, судя, хотя бы, по твоему настойчивому стремлению
исполнять его в неком контексте.

--
Regards, LVT.


 
Leonid Troyanovsky ©   (2009-11-24 12:50) [13]


> SPeller ©   (24.11.09 10:34) [8]

> А оно будет работать когда поток в ожидании оконного сообщения
> и чужим программистом не было предусмотрено что кто-то захочет
> залезть в поток?


> так и быть в длл, в том числе динамически загружаемой (в
> этом случае поток, инициализирующий длл, не обязан быть
> главным потоком приложения,

Вот оно, как. "Мой процесс".
А выглядит как ворованный.

--
Regards, LVT.


 
SPeller ©   (2009-11-24 13:50) [14]


> Вот оно, как. "Мой процесс".
> А выглядит как ворованный.
Я пакостями не занимаюсь, мне не 17 лет :) В проектах просто могу разных использовать, которые могут быть написаны на чем угодно. Так и быть, скажу что пишу аналог стандартного кома. Дком уже готов. Для чего? Позанимавшись любовью с попытками победить настройки безопасности стандартного дкома понял что проще написать один раз своё, чем постоянно заниматься интимом с пользователями, у которых постоянно будут возникать проблемы. А так одно сетевое соединение вместо россыпи соединений для каждого удаленного интерфейса, плюс полный контроль трафика, возможность работы хоть по хттп-хмл, хоть через почту. А общение с точки зрения программы - как с обычным ком объектом. Как там вызовы убегают на удаленную машину - не основной программы забота. И не ее забота где объект физически: в одном процессе или на другом конце города. Кроме того, никаких записей в реестре, что очень хорошо сказывается на юзабилити: скачал и запустил, особенно когда нет админских прав. До сего момента всё работало в куче разных рабочих потоков и возникли ненужные костыли когда некий объект используется в программе, взаимодействует с гуем, и при этом является ком-сервером. Когда к нему приходит уведомление и нужно отреагировать на гуе, то оченама линива каждый раз писать диспетчирование сообщений в основной форме. Хочется в методе, вызванном в моем объекте извне, просто написать DialogForm.ShowModal, к примеру, и всё, и не париться ничем, особенно когда не известно, где клиент - создан тут же формой, локально, или он удаленный и вызов идет из глубин клиент-сервера, диспетчирующего сетевое взаимодействие пулом рабочих потоков. Так вот, сабж затеян для того, чтобы организовать свою реализацию апартмента, чтобы незаметно для основного приложения создавать окно в основном потоке в заранее неопределенный момент, чтобы все обращения к моему объекту прозрачно шли через это окно и не вызывали проблем в работе с основным потоком и гуем, чтобы не думать вообще ни о каком межпотоковом взаимодействии и защите данных, как будто нет никаких потоков. Стандартный ком этим тоже занимается, но мне нужна своя реализация по причинам, описанным выше. Поэтому если я чересчур сложно взялся за решение задачи, то можно было спокойно сказать что это всё лишнее и вот так-то гораздо проще и элегантнее, подсказав что нужно использовать, а не заниматься стёбом "Тоже мне бином Ньютона", "Ой, как все запущено", "Так перректально работать", "Что понравится, то и читай. Там и рядом еще много всякой мудрости", "Все не так", "А выглядит как ворованный".


> Глумлений же, IMO, пока не допускал
Это тебе так кажется. На дельфимастере особая манера общения мастеров и их подражателей с теми, кто из-за экрана монитора и по посту на форуме показался не достоин высочайшего снисхождения в виде нормального общения и дискуссии. Но это уже оффтопик.


 
SPeller ©   (2009-11-24 13:55) [15]


> Для создания в произвольном GUI thread окна много усилий
> не требуется
А сразу указать где оно, которое не требует усилий, нельзя было? Мне гугл на запросы выдавал только тысячи копипастов как создать поток в чужом процессе, как загрузить в него длл, но ни одного варианта как из соседнего потока создать в другом окно. Может, я неправильно искал, конечно, не спорю. Но и на это можно было спокойно указать, без высокомерия.


 
имя   (2009-11-24 14:05) [16]

Удалено модератором


 
SPeller ©   (2009-11-24 14:19) [17]

прошу не развивать здесь оффтопик



Страницы: 1 вся ветка

Форум: "WinAPI";
Текущий архив: 2013.03.22;
Скачать: [xml.tar.bz2];

Наверх




Память: 0.55 MB
Время: 0.068 c
15-1351679340
AV
2012-10-31 14:29
2013.03.22
Com(_to_(USB)). Безобразие..


3-1277112330
alexnauz
2010-06-21 13:25
2013.03.22
Как заставить IBQuery или OraQuery вернуть пустой результат


15-1340596830
ArchValentin
2012-06-25 08:00
2013.03.22
Обучение


15-1347362518
Дмитрий С
2012-09-11 15:21
2013.03.22
Табличка "Туалет занят"


2-1341059064
Начинающий41
2012-06-30 16:24
2013.03.22
DBEDIT





Afrikaans Albanian Arabic Armenian Azerbaijani Basque Belarusian Bulgarian Catalan Chinese (Simplified) Chinese (Traditional) Croatian Czech Danish Dutch English Estonian Filipino Finnish French
Galician Georgian German Greek Haitian Creole Hebrew Hindi Hungarian Icelandic Indonesian Irish Italian Japanese Korean Latvian Lithuanian Macedonian Malay Maltese Norwegian
Persian Polish Portuguese Romanian Russian Serbian Slovak Slovenian Spanish Swahili Swedish Thai Turkish Ukrainian Urdu Vietnamese Welsh Yiddish Bengali Bosnian
Cebuano Esperanto Gujarati Hausa Hmong Igbo Javanese Kannada Khmer Lao Latin Maori Marathi Mongolian Nepali Punjabi Somali Tamil Telugu Yoruba
Zulu
Английский Французский Немецкий Итальянский Португальский Русский Испанский