|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
This is a chapter excerpt from C++/CLI in Action authored by Nishant Sivakumar and published by Manning Publications. The content has been reformatted for CodeProject and may differ in layout from the printed book and the e-book. 4.1 Using interior and pinning pointersYou can't use native pointers with CLI objects on the managed heap. That is like trying to write Hindi text using the English alphabet—they're two different languages with entirely different alphabets. Native pointers are essentially variables that hold memory address locations. They point to a memory location rather than to a specific object. When we say a pointer points to an object, we essentially mean that a specific object is at that particular memory location. This approach won't work with CLI objects because managed objects in the CLR heap don't remain at the same location for the entire period of their lifetime. Figure 4.1 shows a diagrammatic view of this problem. The Garbage Collector (GC) moves objects around during garbage-collection and heap-compaction cycles. A native pointer that points to a CLI object becomes garbage once the object has been relocated. By then, it's pointing to random memory. If an attempt is made to write to that memory, and that memory is now used by some other object, you end up corrupting the heap and possibly crashing your application.
C++/CLI provides two kinds of pointers that work around this problem. The first kind is called an interior pointer, which is updated by the runtime to reflect the new location of the object that's pointed to every time the object is relocated. The physical address pointed to by the interior pointer never remains the same, but it always points to the same object. The other kind is called a pinning pointer, which prevents the GC from relocating the object; in other words, it pins the object to a specific physical location in the CLR heap. With some restrictions, conversions are possible between interior, pinning, and native pointers. Pointers by nature aren't safe, because they allow you to directly manipulate
memory. For that reason, using pointers affects the type-safety and
verifiability of your code. I strongly urge you to refrain from using CLI
pointers in pure-managed applications (those compiled with 4.1.1 Interior pointersAn interior pointer is a pointer to a managed object or a member of a managed object that is updated automatically to accommodate for garbage-collection cycles that may result in the pointed-to object being relocated on the CLR heap. You may wonder how that's different from a managed handle or a tracking reference; the difference is that the interior pointer exhibits pointer semantics, and you can perform pointer operations such as pointer arithmetic on it. Although this isn't an exact analogy, think of it like a cell phone. People can call you on your cell phone (which is analogous to an interior pointer) wherever you are, because your number goes with you—the mobile network is constantly updated so that your location is always known. They wouldn't be able to do that with a landline (which is analogous to a native pointer), because a landline's physical location is fixed. Interior pointer declarations use the same template-like syntax that is used for CLI arrays, as shown here: interior_ptr< type > var = [address];
Listing 4.1 shows how an interior pointer gets updated when the object it points to is relocated. ref struct CData
{
int age;
};
int main()
{
for(int i=0; i<100000; i++) // ((1))
gcnew CData();
CData^ d = gcnew CData();
d->age = 100;
interior_ptr<int> pint = &d->age; // ((2))
printf("%p %d\r\n",pint,*pint);
for(int i=0; i<100000; i++) // ((3))
gcnew CData();
printf("%p %d\r\n",pint,*pint); // ((4))
return 0;
}
Listing 4.1 Code that shows how an interior pointer is updated by the CLRIn the sample code, you create 100,000 orphan 012CB4C8 100
012A13D0 100
As you can see, the address pointed to by the interior pointer has changed. Had this been a native pointer, it would have continued to point to the old address, which may now belong to some other data variable or may contain random data. Thus, using a native pointer to point to a managed object is a disastrous thing to attempt. The compiler won't let you do that: You can't assign the address of a CLI object to a native pointer, and you also can't convert from an interior pointer to a native pointer. Passing by referenceAssume that you need to write a function that accepts an integer (by reference) and changes that integer using some predefined rule. Here's what such a function looks like when you use an interior pointer as the pass-by-reference argument: void ChangeNumber(interior_ptr<int> num, int constant)
{
*num += constant * *num;
}
And here's how you call the function: CData^ d = gcnew CData();
d->age = 7;
interior_ptr<int> pint = &d->age;
ChangeNumber(pint, 3);
Console::WriteLine(d->age); // outputs 28
Because you pass an interior pointer, the original variable (the age member
of the int number = 8;
ChangeNumber(&number, 3); // ((1)) Pass native pointer to function
Console::WriteLine(number); // outputs 32
It's imperative that you remember this. You can pass a native pointer to function that expects an interior pointer as you do here ((1)), because there is an implicit conversion from the interior pointer to the native pointer. But you can't pass an interior pointer to a native pointer; if you try that, you'll get a compiler error. Because native pointers convert to interior pointers, you should be aware that an interior pointer need not necessarily always point to the CLR heap: If it contains a converted native pointer, it's then pointing to the native C++ heap. Next, you'll see how interior pointers can be used in pointer arithmetic (something that can't be done with a tracking reference). Pointer arithmeticInterior pointers (like native pointers) support pointer arithmetic; thus,
you may want to optimize a performance-sensitive piece of code by using direct
pointer arithmetic on some data. Here's an example of a function that uses
pointer arithmetic on an interior pointer to quickly sum the contents of an
array of int SumArray(array<int>^% intarr)
{
int sum = 0;
interior_ptr<int> p = &intarr[0]; // ((1)) Get interior pointer to array
while(p != &intarr[0]+ intarr->Length) // ((2)) Iterate through array
sum += *p++;
return sum;
}
In this code, It's
not just arrays that can be manipulated using an interior pointer. Here's
another example of using an interior pointer to manipulate the contents of a
StString^ str = "Nish wrote this book for Manning Publishing";
interior_ptr<Char> ptxt = const_cast< interior_ptr<Char> >(
PtrToStringChars(str)); // ((1))
interior_ptr<Char> ptxtorig = ptxt; // ((2))
while((*ptxt++)++); // ((3))
Console::WriteLine(str); // ((4))
while((*ptxtorig++)--); // ((5))
Console::WriteLine(str); // ((6))
You use the Ojti!xspuf!uijt!cppl!gps!Nboojoh!Qvcmjtijoh
You've achieved encryption! (Just kidding.) Because you saved the original
pointer in Nish wrote this book for Manning Publishing
Whenever you use an interior pointer, it's represented as a managed pointer
in the generated MSIL. To distinguish it from a reference (which is also
represented as a managed pointer in IL), a value class V
{
void Func()
{
interior_ptr<V> pV1 = this;
//V* pV2 = this; <-- this won't compile
}
};
As is obvious, in a 4.1.2 Pinning pointersAs we discussed in the previous section, the GC moves CLI objects around the CLR heap during garbage-collection cycles and during heap-compaction operations. Native pointers don't work with CLI objects, for reasons previously mentioned. This is why we have interior pointers, which are self-adjusting pointers that update themselves to always refer to the same object, irrespective of where the object is located in the CLR heap. Although this is convenient when you need pointer access to CLI objects, it only works from managed code. If you need to pass a pointer to a CLI object to a native function (which runs outside the CLR), you can't pass an interior pointer, because the native function doesn't know what an interior pointer is, and an interior pointer can't convert to a native pointer. That's where pinning pointers come into play. A pinning pointer pins a CLI object on the CLR heap; as long as the pinning pointer is alive (meaning it hasn't gone out of scope), the object remains pinned. The GC knows about pinned objects and won't relocate pinned objects. To continue the phone analogy, imagine a pinned pointer as being similar to your being forced to remain stationary (analogous to being pinned). Although you have a cell phone, your location is fixed; it's almost as if you had a fixed landline. Because pinned objects don't move around, it's legal to convert a pinned pointer to a native pointer that can be passed to the native caller that's running outside the control of the CLR. The word pinning or pinned is a good choice; try to visualize an object that's pinned to a memory address, just like you pin a sticky note to your cubicle's side-board. The syntax used for a pinning pointer is similar to that used for an interior pointer: pin_ptr< type > var = [address];
The duration of pinning is the lifetime of the pinning pointer. As long as
the pinning pointer is in scope and pointing to an object, that object remains
pinned. If the pinning pointer is set to Listing 4.2 demonstrates the difference between interior and pinning
pointers. To simulate a real-world scenario within a short code snippet, I used
for(int i=0; i<100000; i++)
gcnew CData(); // Fill portion of CLR Heap
CData^ d1 = gcnew CData(); // ((1))
for(int i=0; i<1000; i++)
gcnew CData();
CData^ d2 = gcnew CData();
interior_ptr<int> intptr = &d1->age; // ((2))
pin_ptr<int> pinptr = &d2->age; // ((3))
printf("intptr=%p pinptr=%p\r\n", // Display pointer addresses before GC
intptr, pinptr);
for(int i=0; i<100000; i++) // ((4))
gcnew CData();
printf("intptr=%p pinptr=%p\r\n",
intptr, pinptr); // Display pointer addresses after GC
Listing 4.2 Code that compares an interior pointer with a pinning pointerIn the code, you create two intptr=012CB4C8 pinptr=012CE3B4
intptr=012A13D0 pinptr=012CE3B4
Your pointer addresses will be different, but after the garbage-collection
cycle, you'll find that the address held by the pinned pointer ( Passing to native codeThe fact that a pinning pointer always points to the same object (because the
object is in a pinned state) allows the compiler to provide an implicit
conversion from a pinning pointer to a native pointer. Thus, you can pass a
pinning pointer to any native function that expects a native pointer, provided
the pointers are of the same type. Obviously, you can't pass a pinning pointer
to a #pragma unmanaged
int NativeCountVowels(wchar_t* pString)
{
int count = 0;
const wchar_t* vowarr = L"aeiouAEIOU";
while(*pString)
if(wcschr(vowarr,*pString++))
count++;
return count;
}
#pragma managed
Here's how you pass a pointer to a CLI object, after first pinning it, to the native function just defined: String^ s = "Most people don't know that the CLR is written in C++";
pin_ptr<Char> p = const_cast< interior_ptr<Char> >(
PtrToStringChars(s));
Console::WriteLine(NativeCountVowels(p));
As you can see in the figure, the only pointer conversion that is illegal is that from an interior pointer to a native pointer; every other conversion is allowed and implicitly done. You have seen how pinning pointers make it convenient for you to pass pointers to CLI objects to unmanaged code. I now have to warn you that pinning pointers should be used only when they're necessary, because tactless usage of pinning pointers results in what is called the heap fragmentation problem. The heap fragmentation problemObjects are always allocated sequentially in the CLR heap. Whenever a garbage collection occurs, orphan objects are removed, and the heap is compacted so it won't remain in a fragmented condition. (We covered this in the previous chapter when we discussed the multigenerational garbage-collection algorithm used by the CLR.) Let's assume that memory is allocated from a simple heap that looks like figures 4.3 through 4.6. Of course, this is a simplistic representation of the CLR's GC-based memory model, which involves a more complex algorithm. But the basic principle behind the heap fragmentation issue remains the same, and thus this simpler model will suffice for the present discussion. Figure 4.3 depicts the status of the heap before a garbage-collection cycle occurs.
There are presently three objects in the heap. Assume that
The orphan object has been removed and a heap compaction has been performed,
so
Assume that
None of those pinned objects can be relocated. This means the compaction process can't be effectively implemented. When there are several such pinned objects, the heap is severely fragmented, resulting in slower and less efficient memory allocation for new objects. This is the case because the GC has to try that much harder to find a block that's large enough to fit the requested object. Sometimes, although the total free space is bigger than the requested memory, the fact that there is no single continuous block of memory large enough to hold that object results in an unnecessary garbage-collection cycle or a memory exception. Obviously, this isn't an efficient scenario, and it's why you have to be extremely cautious when you use pinning pointers. Recommendations for using pinning pointersNow that you've seen where pinning pointers can be handy and where they can be a little dodgy, I'm going to give you some general tips on effectively using pinning pointers.
Note that these are general guidelines and not hard rules to be blindly followed at all times. It's good to have some basic strategies and to understand the exact consequences of what happens when you inappropriately use pinning pointers. Eventually, you have to evaluate your coding scenario and use your judgment to decide on the best course.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||