인터넷/IT/프로그래밍

TXMLDocument 클래스 사용 시 반드시 선언은 IXMLDocument로 하자

coffee94 2014. 3. 13. 09:00

예를 들어 다음과 같은 코드는 문제가 발생할 수 있습니다.


procedure TForm1.FormCreate(Sender: TObject);
var
  I, iCount: Integer;
  XMLDocument: TXMLDocument;
begin
  XMLDocument := TXMLDocument.Create(nil);

  XMLDocument.LoadFromFile('mytest.xml');

  iCount := XMLDocument.DocumentElement.ChildNodes.Count;
  for I := 0 to iCount - 1 do
  begin
    ShowMessage(XMLDocument.DocumentElement.ChildNodes[I].NodeName);
  end;
end;


변수 선언 타입이 IXMLDocument로 되어 있지 않고 TXMLDocument로 되어 있는데 간혹 인터넷을 찾아보면 저런 코드가 많은 것 같네요..


해당 코드는 XMLDocument.DocumentElement.ChildNodes.Count; 부분이 실행되거나 또는 해당 위치에 브레이크 포인트를 걸고 Count  값을 확인할 경우 XMLDocument 변수는 잘못된 값을 가르키게 됩니다. ( 보통 저렇게 코드를 작성해도 운이 좋게 실행되는 경우가 있기 때문에 발견하기 힘듭니다. )


문제가 발생하는 원리를 찾아보면...

우선 XMLDocument := TXMLDocument.Create(nil); 부분은 다음과 같은 코드가 실행됩니다.


function _ClassCreate(InstanceOrVMT: Pointer; Alloc: ShortInt): TObject;
{$IFNDEF CPUX86}
begin
  if Alloc >= 0 then
    InstanceOrVMT := Pointer(TClass(InstanceOrVMT).NewInstance);
  Result := TObject(InstanceOrVMT);
end;
{$ELSE CPUX86}
asm
        { ->    EAX = pointer to VMT      }
        { <-    EAX = pointer to instance }
        PUSH    EDX
        PUSH    ECX
        PUSH    EBX
        TEST    DL,DL
        JL      @@noAlloc
        CALL    DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance
@@noAlloc:
{$IFDEF STACK_BASED_EXCEPTIONS}
        XOR     EDX,EDX
        LEA     ECX,[ESP+16]
        MOV     EBX,FS:[EDX]
        MOV     [ECX].TExcFrame.next,EBX
        MOV     [ECX].TExcFrame.hEBP,EBP
        MOV     [ECX].TExcFrame.desc,offset @desc
        MOV     [ECX].TexcFrame.ConstructedObject,EAX   { trick: remember copy to instance }
        MOV     FS:[EDX],ECX
{$ENDIF STACK_BASED_EXCEPTIONS}
        POP     EBX
        POP     ECX
        POP     EDX
        RET

{$IFDEF STACK_BASED_EXCEPTIONS}
@desc:
        JMP     _HandleAnyException

  {       destroy the object                                                      }

        MOV     EAX,[ESP+8+9*4]
        MOV     EAX,[EAX].TExcFrame.ConstructedObject
        TEST    EAX,EAX
        JE      @@skip
        MOV     ECX,[EAX]
        MOV     DL,$81
        PUSH    EAX
        CALL    DWORD PTR [ECX] + VMTOFFSET TObject.Destroy
        POP     EAX
        CALL    _ClassDestroy
@@skip:
  {       reraise the exception   }
        CALL    _RaiseAgain
{$ENDIF STACK_BASED_EXCEPTIONS}
end;
{$ENDIF CPUX86}


class function TXMLDocument.NewInstance: TObject;
begin
  Result := inherited NewInstance;
  TXMLDocument(Result).FRefCount := 1;
end;


그리고 생성자 호출 이후에 바로 다음 함수가 호출됩니다.

function _AfterConstruction(Instance: TObject): TObject;
begin
  try
    Instance.AfterConstruction;
    Result := Instance;
  except
    _BeforeDestruction(Instance, 1);
    raise;
  end;
end;


procedure TXMLDocument.AfterConstruction;
begin
  inherited;
  if (csDesigning in ComponentState) and not (csLoading in ComponentState) then
    DOMVendor := GetDOMVendor(DefaultDOMVendor);
  FOptions := [doNodeAutoCreate, doAttrNull, doAutoPrefix, doNamespaceDecl];
  NSPrefixBase := 'NS';
  NodeIndentStr := DefaultNodeIndent;
  FOwnerIsComponent := Assigned(Owner) and (Owner is TComponent);
  FXMLStrings := TStringList.Create;
  FXMLStrings.OnChanging := XMLStringsChanging;
  if FFileName <> '' then
    SetActive(True);
  TInterlocked.Decrement(FRefCount);
end;


위에 보시면 TInterlocked.Decrement(FRefCount); 하는 부분이 있는데 이미 여기서부터 문제의 조짐이 발생됨을 볼 수 있습니다. FRefCount가 생성자가 호출 될 때 1이었는데 여기서 값을 감소시키므로 0이 되어버립니다.


사실 여기까지는 괞찮은데 대입되는 변수가 인터페이스가 아니라 클래스 타입이므로 레퍼런스 카운트는 클래스에 대입되므로 참조 카운트가 증가하지 않고 그대로 0이 되고, 소멸자가 호출되지 않았기 때문에 객체가 해제가 되지 않을 뿐 결국엔 XMLDocument.DocumentElement.ChildNodes.Count 루틴이 호출되면서 레퍼런스 카운트가 0이 되면서 소멸자가 호출되고 해제 된 메모리 객체를 가르키게 되는 것입니다.


만약 IXMLDocument로 선언할 경우 위와 똑같이 실행되지만 추가로 AfterConstruction  함수가 호출 된 이후에 아래와 같이 내부적으로 _IntfCopy 함수인 인터페이스를 복사하는 함수가 호출되어 다시 레퍼런스 카운트를 1로 만들게 됩니다.


procedure _IntfCopy(var Dest: IInterface; const Source: IInterface);
{$IFDEF PUREPASCAL}
var
  P: Pointer;
begin
  P := Pointer(Dest);
  if Source <> nil then
    Source._AddRef;
  Pointer(Dest) := Pointer(Source);
  if P <> nil then
    IInterface(P)._Release;
end;
{$ELSE}
asm
{
  The most common case is the single assignment of a non-nil interface
  to a nil interface.  So we streamline that case here.  After this,
  we give essentially equal weight to other outcomes.

    The semantics are:  The source intf must be addrefed *before* it
    is assigned to the destination.  The old intf must be released
    after the new intf is addrefed to support self assignment (I := I).
    Either intf can be nil.  The first requirement is really to make an
    error case function a little better, and to improve the behaviour
    of multithreaded applications - if the addref throws an exception,
    you don't want the interface to have been assigned here, and if the
    assignment is made to a global and another thread references it,
    again you don't want the intf to be available until the reference
    count is bumped.
}
        TEST    EDX,EDX         // is source nil?
        JE      @@NilSource
        PUSH    EDX             // save source
        PUSH    EAX             // save dest
        MOV     EAX,[EDX]       // get source vmt
        PUSH    EDX             // source as arg
        CALL    DWORD PTR [EAX] + VMTOFFSET IInterface._AddRef
        POP     EAX             // retrieve dest
        MOV     ECX, [EAX]      // get current value
        POP     [EAX]           // set dest in place
        TEST    ECX, ECX        // is current value nil?
        JNE     @@ReleaseDest   // no, release it
        RET                     // most common case, we return here
@@ReleaseDest:
{$IFDEF ALIGN_STACK}
        SUB     ESP, 8
{$ENDIF ALIGN_STACK}
        MOV     EAX,[ECX]       // get current value vmt
        PUSH    ECX             // current value as arg
        CALL    DWORD PTR [EAX] + VMTOFFSET IInterface._Release
{$IFDEF ALIGN_STACK}
        ADD     ESP, 8
{$ENDIF ALIGN_STACK}
        RET

{   Now we're into the less common cases.  }
@@NilSource:
        MOV     ECX, [EAX]      // get current value
        TEST    ECX, ECX        // is it nil?
        MOV     [EAX], EDX      // store in dest (which is nil)
        JE      @@Done
        MOV     EAX, [ECX]      // get current vmt
{$IFDEF ALIGN_STACK}
        SUB     ESP, 8
{$ENDIF ALIGN_STACK}
        PUSH    ECX             // current value as arg
        CALL    DWORD PTR [EAX] + VMTOFFSET IInterface._Release
{$IFDEF ALIGN_STACK}
        ADD     ESP, 8
{$ENDIF ALIGN_STACK}
@@Done:
end;
{$ENDIF !PUREPASCAL}



따라서 문제가 발생하지 않겠죠.

정말 간단한 부분인데 놓치기 쉬운 부분이기도 합니다.