void *CoTaskMemRealloc(void *pv,DWORD cb);
// grow/shrink *pv
// расширяем/сжимаем *pv
Семантика этих трех функций такая же, как у их эквивалентов из динамической библиотеки С: malloc, free и realloc. Разница состоит в том, что они предназначены исключительно для выделения памяти параметрам типа вложенных указателей с атрибутами [out] и [in,out]. Другое важное отличие состоит в том, что подпрограммы из динамической библиотеки С нельзя использовать для выделения памяти в одном модуле и освобождения ее в другом. Дело в том, что детали реализации каждой динамической библиотеки С являются специфическими и изменяются при смене компилятора. Так как все участники согласились использовать один и тот же распределитель, предлагаемый СОМ, нет проблемы с освобождением клиентом памяти, которая выделена объектом, скомпилированным в отдельной DLL.
Чтобы понять, как используются на практике блоки памяти, выделенные вызываемым оператором, рассмотрим приводившийся ранее метод GetFromPound:
HRESULT GetFromPound([out] DOG *pDog);
В то время как память для объекта DOG должна быть выделена вызывающей программой (pDog является указателем высшего уровня), память для объекта HUMAN должна быть выделена реализацией метода с использованием распределителя памяти задачи (pDog->pOwner является вложенным в [out]-параметр указателем). Реализация метода выглядела бы примерно так:
STDMETHODIMP GetFromPound(/*[out]*/DOG *pDog)
{
short did = LookupNewDogId();
short hid = LookupHumanId(did);
pDog->nDogID = did;
// allocate memory for embedded pointer
// выделяем память для вложенного указателя
pDog->pOwner = (HUMAN*) CoTaskMemAlloc(sizeof(HUMAN));
if (pDog->pOwner == 0)
// not enough memory
// недостаточно памяти
return R_OUTOFMEMORY;
pDog->pOwner->nHumanID = hid;
return S_OK;
}
Отметим, что метод возвращает специальный HRESULT E_OUTOFMEMORY, указывающий на то, что операция прервана из-за нехватки памяти.
Программа, вызывающая метод GetFromPound, ответственна за освобождение любой памяти, выделенной вызываемым методом, после использования соответствующих значений:
DOG fido;
HRESULT hr = p->GetFromPound(&fido);
if (SUCCEEDED(hr)) {
printf(«The dog %h is owned by %h», fido.nDogID, fido.pOwner->nHumanID);
// data has been consumed, so free the memory
// данные использованы, поэтому освобождаем память
CoTaskMemFree(fido.pOwner);
}
В случае сбоя метода клиент может предположить, что не было выделено никакой памяти, если только в документации не указан другой исход.
В только что приведенном примере использован чистый [out]-параметр. Управление [in, out]– параметрами несколько более сложно. Вложенные указатели для [in, out]-параметров должны быть размещены вызывающей программой с помощью распределителя памяти задачи. Если методу требуется повторно распределить память, переданную клиентом, то метод должен сделать это с использованием CoTaskMemRealloc. Если же вызывающая программа не имеет никакой информации для передачи методу, то она может передать ему на входе нулевой указатель, и тогда метод может использовать CoTaskMemRealloc (который без проблем принимает нулевой указатель и делает то, что нужно). Подобным же образом, если у метода нет информации для обратной передачи в вызывающую программу, он может просто освободить память, на которую ссылается вложенный указатель. Рассмотрим следующее определение метода IDL:
HRESULT SendToVet([in, out] DOG *pDog);
Пусть у вызывающей программы имеется легальное значение HUMAN, которое она хочет передать как параметр. Тогда клиентский код может выглядеть примерно так:
HUMAN *pHuman = (HUMAN*)CoTaskMemAllocc(sizeof(HUMAN));
pHuman->nHumanID = 1522;
DOG fido = { 4111, pHuman };
HRESULT hr = p->SendToVet(&fido); // [in, out]
if (SUCCEEDED(hr)) {
if (fido.pOwner)
printf(«Dog is now owned by %h», fido.pOwner->nHumanID);
CoTaskMemFree(fido.pOwner);
// OK to free null ptr.
// можно освободить нулевой указатель
}
Реализация метода могла бы повторно использовать буфер, используемый вызывающей программой, или выделить новый буфер в случае, если вызывающая программа передала нулевой вложенный указатель:
STDMETHODIMP MyClass::SendToVet(/*[in, out]*/DOG *pDog)
{
if (fido.pOwner == 0)
fido.pOwner = (HUMAN*)CoTaskMemAlloc(sizeof (HUMAN));
if (fido.pOwner == 0)
// alloc failed
// сбой выделения памяти
return E_OUTOFMEMORY;
fido.pOwner->nHumanID = 22;
return S_OK;
}
Поскольку работа с [in,out]-параметрами в качестве вложенных указателей имеет ряд тонкостей, в документации на интерфейс часто повторяются правила управления памятью для вложенных указателей.
Приведенные выше фрагменты кода используют наиболее удобный интерфейс для СОМ-распределителя памяти задач. До появления версии СОМ под Windows NT основная связь с распределителем памяти задачи осуществлялась через его интерфейс IMallос:
[ uuid(00000002-0000-0000-C000-000000000046),local,object]
interface IMalloc : IUnknown {
void *Alloc([in] ULONG cb);
void *Realloc ([in, unique] void *pv, [in] ULONG cb);
void Free([in, unique] void *pv);
ULONG GetSize([in, unique] void *pv);
int DidAlloc([in, unique] void *pv);
void HeapMinimize(void);
}
Для получения доступа к интерфейсу IMalloc распределителя памяти задачи в СОМ имеется API-функция CoGetMalloc:
HRESULT CoGetMalloc(
[in] DWORD dwMemCtx, // reserved, must be one
// зарезервировано, должно равняться единице
[out] IMalloc **ppMalloc); // put it here!
// помещаем его здесь!
Это означает, что вместо вызова удобного метода CoTaskMemAlloc:
HUMAN *pHuman = (HUMAN*)CoTaskMemAlloc(sizeof(HUMAN));
можно использовать следующую менее удобную форму:
IMalloc *pMalloc = 0;
pHuman = 0;
HRESULT hr = CoGetMalloc(1, &pMalloc);
if (SUCCEEDED(hr)) {
pHuman = (HUMAN*)pMalloc->Alloc(sizeof(HUMAN));
pMalloc->Release();
}
Преимущество последней технологии заключается в том, что она совместима с ранними, до Windows NT, версиями СОМ. Но в целом предпочтительнее использовать CoTaskMemAlloc и другие, поскольку эти методы требуют меньше программного кода и поэтому меньше подвержены ошибкам программирования.
До сих пор обсуждение распределителя памяти задачи было сфокусировано на вопросах, как и когда объекты выделяют память, а клиенты – освобождают ее. Однако не обсуждалось, что происходит, когда объект и клиент размещаются в различных адресных пространствах. Это во многом связано с отсутствием различия в способах реализации клиентов и объектов при использовании интерфейсных маршалеров. СОМ-распределитель памяти задачи получает свою память из закрытого адресного пространства процессов. С учетом этого сокрытие того обстоятельства, что распределитель памяти задачи не может охватить оба адресных пространства, является делом интерфейсной заглушки и интерфейсного заместителя. Когда интерфейсная заглушка вызывает метод объекта, она маршалирует любые [out]– или [in, out]-параметры в ответное ORPC-сообщение. Как показано на рис. 7.1, по завершении этого маршалинга интерфейсная заглушка (которая в конечном счете является внутриапартаментным клиентом данного объекта) освобождает с помощью метода CoTaskMemFree любую память, выделенную вызываемой программой. Это эффективно освобождает всю память, выделенную в течение вызова метода внутри адресного пространства объекта. При получении ответного ORPC-сообщения интерфейсный заместитель с помощью метода CoTaskMemAlloc выделяет пространство для всех параметров, размещаемых в вызываемой программе.