четверг, 19 марта 2009 г.

Указатель на метод интерфейса

Понадобилось на днях коллегам передать в качестве callback-функции метод объекта, доступ к которому есть только через интерфейс. Показалось, что проблемы особой нет - интерфейс "знает" о своем объекте, осталось вычислить адрес объекта да самого метода.

Но реальность оказалась не столь оптимистичной - на текущий момент Delphi такого функционала не поддерживает:
  • http://groups.google.com.ua/group/borland.public.delphi.objectpascal/browse_thread/thread/4c5dcb094b82522f?pli=1
  • http://qc.embarcadero.com/wc/qcmain.aspx?d=941 (а судя по датам и не будет поддерживать)
Самому раньше в таком функционале необходимости не было - проще передавать весь интерфейс, но если необходимо взаимодействие с чужим кодом, где могут требовать именно указатель на функцию, то прийдется изобретать не очень стройные решения:
  • метод в интерфейсе напрямую возвращающий адрес метода объекта
IAnIntf = interface
function GetPointerToProc: TAnObjectProc:
end
  • объект-обертку над интерфейсом
TWrapObj = class
private
AnIntf: ITheIntf;
protected
procedure AnProc(const Value: string);
end;

procedure TWrapObj.AnProc(const Value: string);
begin
AnIntf.AnProc(Value);
end;
  • возможно как-то можно будет выкрутиться через анонимные функции? нельзя, ибо
    TAnProc = procedure (S: string) of object;
    TAnProc2 = reference to procedure (S: string);
    это разные типы
Хотя и в реализации проблем более чем - вычислять статический адрес, на этапе компиляции, для такого указателя не получится, т.к. один и тот же интерфейс может реализовываться разными объектами.

> Still I think making (procedure of object) and (procedure of Interface) equivalent
> typeswould be the better solution.

Like I said, the semantics for the compiler would be quite different, so
it would make sense to make them different types (even if both
represented internally by a TMethod). Actually, if the interface
instance variable is nil, it is impossible to get the method address.
This is not a problem for objects, since there the static address is
taken.

4 комментария:

Анонимный комментирует...

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

А теперь мое решение :) Всего 14 строк кода, учитывая комментарии и пустые строки. Как говорится, I challenge you...

program Project1;

{$APPTYPE CONSOLE}

type
TProc1 = procedure (const S:string; i:integer) of object;

IIntf = interface(IUnknown)
procedure IntfProc(const S:string; i:integer);
end;

TObj=class(TInterfacedObject,IIntf)
procedure IntfProc(const S:string; i:integer);

destructor Destroy;override;
end;

//==============код адаптера=============================
procedure Adapter; assembler;
asm
// само собой тип вызова fastcall :)
MOV EBX, [EAX]
JMP DWORD PTR [EBX] + VMTOFFSET IIntf.IntfProc
end;

function ToObj(I:IIntf):TProc1;
begin
//строка для проверки совместимости типов callback'а и интерфейса
if false then Result:=TObj(nil).IntfProc;

TMethod(Result).Code:=@Adapter;
TMethod(Result).Data:=Pointer(I);
end;
//==============код адаптера=============================

{ TObj }

destructor TObj.Destroy;
begin
Writeln('Destructor called');
inherited;
end;

procedure TObj.IntfProc(const S: string; i: integer);
begin
Writeln(S, ' ', i);
end;

procedure Test(P:TProc1);
//callback emulator
begin
P('Test', 1);
end;

var O:TObj;
I:IIntf;
begin
O:=TObj.Create;
I:=O;
I.IntfProc('ss', 1);
Test( ToObj(I) );
end.

pnv82 комментирует...

Гм, но о живучести интерфейса, ежели где этот указатель запоминать, заботиться прийдется самому. Или делать детскую обертку...
Ибо по выходу из ToObj ссылок на интерфейс уже не будет, так?

И попутно вопрос, насколько корректно в TMethod(Result).Data класть указатель на интерфейс, если в самой функции будут обращения к Self?

Анонимный комментирует...

По первому вопросу - "живучесть" интерфейса как обеспечивалась программистом, так и должна обеспечиваться. Ситуация, когда в callback'е есть указатель на функцию, интерфейс которой уже невалиден, должна и будет наказываться исключением :)
При большом желании можно сделать AddRef, только это все равно ручная работа. Да и странно это.

По второму - интерфейс и есть указатель, хоть как ты его заворачивай и обзывай. С обращениями к Self тоже проблем быть не должно (тестил). Если для вызова метода, как по-умолчанию, используется fastcall.

Так что я и дальше (пока) продолжаю считать мое решение оптимальным ;)

pnv82 комментирует...

Если на метод интерфейса есть ссылка, то вполне логично держать счетчик у самого интерфейса.

А для чего используется TMethod(Result).Data? Только для отсчета смещения? Ведь ссылка на интерфейс и сам Self это вроде разные адреса?