Chapter 5: Developing Phase: Memory and File Management

This chapter discusses the programming differences between the Microsoft® .NET Framework and the UNIX environment in the following two categories:

  • Memory management

  • File management

In addition, this chapter outlines the various options available for converting the UNIX code to Microsoft .NET in each of these categories and illustrates the options with appropriate source code examples.

*

On This Page

Memory Management Memory Management
File Management File Management

Memory Management

This section explains the process of memory allocation, de-allocation on the managed heap, and the garbage collection mechanism in the .NET environment. The following topics are explained in detail:

  • Allocating memory

  • Releasing memory

  • Garbage collection

  • Releasing unmanaged resources

  • Thread local storage

UNIX provides the standard heap management functions for memory management. The standard C runtime on UNIX includes such functions as calloc() and malloc() for allocating memory and free() for de-allocating memory. The programmer, however, has to delete the references that are no longer required. Simply put, the programmer has to free the allocated memory when references to that memory are no longer required. Otherwise, it could lead to memory leaks.

In the .NET Framework, memory management is automatic. The garbage collector, which is a part of the common language runtime (CLR), manages the allocation and release of memory for managed code. Automatic memory management in .NET eliminates some common problems, such as forgetting to free a reference that causes memory leaks or attempting to access memory for an object that has already been freed. The following section describes how the garbage collector allocates and releases the memory for an application.

Allocating Memory

When a new process is initialized, the runtime reserves a contiguous region of address space for the process. This reserved address space is called the managed heap. The managed heap maintains a pointer to the address space, which will be allocated to the next object in the heap. Initially, this pointer is set to the base address of the managed heap. All reference types are allocated on the managed heap. When an application creates the first reference type, memory is allocated for the type at the base address of the managed heap. When the application creates the next object, the garbage collector allocates memory for it in the address space immediately following the first object. As long as address space is available, the garbage collector continues to allocate space for new objects in this manner.

Allocating memory from the managed heap is faster than the unmanaged memory allocation. Because the runtime allocates memory for an object by adding a value to a pointer, it is almost as fast as allocating memory from the stack. In addition, because new objects that are allocated address spaces consecutively are stored contiguously in the managed heap, an application can access the objects very quickly.

Releasing Memory

The optimizing engine of the garbage collector determines the best time to perform a collection based on the allocations made. When the garbage collector performs a collection, it releases the memory for the objects that are no longer being used by the application. How the garbage collector identifies the objects that are no longer being used and how the memory is released is discussed in detail in the “Garbage Collection” section later in this chapter.

To improve performance, the runtime allocates memory for large objects in a separate heap. The garbage collector automatically releases the memory allocated for large objects.

Releasing Memory for Unmanaged Resources

The garbage collector automatically performs the necessary memory management tasks for the majority of the objects that an application creates. However, unmanaged resources require an explicit cleanup. When you create an object that encapsulates an unmanaged resource, it is recommended that you provide the necessary code to clean up the unmanaged resource in a public Dispose method. A Dispose method enables users of an object to explicitly free the memory when they are finished with the object.

Garbage Collection

Each time the new operator is used to create an object, the runtime allocates memory for the object from the managed heap. As long as address space is available in the managed heap, the runtime continues to allocate memory for new objects. However, memory is not infinite.

Eventually, the garbage collector must perform a collection to free some memory. The optimizing engine of the garbage collector determines the best time to perform a collection, based upon the allocations made. When the garbage collector performs a collection, it checks the managed heap for objects that are no longer being used by the application and performs the necessary operations to reclaim the memory.

The garbage collector determines which objects are no longer being used by examining the roots of the application. Every application has a set of roots and each root refers to an object on the managed heap. The runtime maintains a list of all the active roots. The garbage collector accesses the list to identify the unused objects, marks these objects for release, and releases the memory allocated for them.

Irrespective of the managed language used, the garbage collector of the .NET Framework provides automatic memory management. It allocates and releases the memory for the managed objects and, when necessary, executes the Finalize methods and destructors to properly clean up the unmanaged resources.

Automatic memory management simplifies development by eliminating the common bugs that arise from the manual memory management schemes. The following steps describe the life cycle of the object from its creation to destruction:

  • Type initialization. When the first instance of an object is created, it executes any shared initialization and shared constructor code.

  • Instance initialization. When an instance of your component is created, data members that have initialization code are initialized, and the appropriate constructor overload is executed.

  • Disposing of resources. If the object overrides the Dispose method, it frees all system resources it may have allocated, releases references to other objects, and renders itself unusable.

  • Instance destruction. When garbage collection detects that there are no remaining references to the component, the runtime calls your component's destructor and frees the memory.

Forcing Garbage Collection

The garbage collection GC class provides the GC.Collect method, which is used to give an application some direct control over the garbage collector. In general, avoid calling any of the collect methods and allow the garbage collector to run independently. In most cases, the garbage collector is better at determining the best time to perform a collection. However, in certain rare situations, forcing a collection might improve the performance of an application.

Use the GC.Collect method in a situation where there is a significant reduction in the amount of memory being used at a defined point in the application code. For example, an application might use a document that references a significant number of unmanaged resources. When the application closes the document, the resources the document has been using are no longer needed. To improve the performance of the application, consider releasing the unused resources.

Note   More information on GC.Collect method is available at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemgcclasscollecttopic.asp.

Releasing Unmanaged Resources

The most common type of an unmanaged resource is an object that wraps an operating system resource, such as a file, a window, or a network connection. Although the garbage collector can track the lifetime of an object that encapsulates an unmanaged resource, it cannot clean up the resource. For these types of objects, the .NET Framework provides the Object.Finalize method, which allows an object to clean up its unmanaged resources properly when the garbage collector reclaims the memory used by the object. In C#, finalizers are expressed using the destructor syntax.

To properly dispose of the unmanaged resources, consider implementing a public Dispose method, which executes the necessary cleanup code for the object. The IDisposable interface in the System namespace provides the Dispose method for the resource classes that implement the interface. This method is public and hence the users of an application can call the Dispose method directly to free the memory used by the unmanaged resources. If a Dispose method is properly implemented, the destructor or the finalize method becomes a safeguard to clean up the unmanaged resources in case the Dispose method is not called.

The Dispose method of an object type should release all the resources that it owns. In addition, it should also release all the resources owned by its base types by calling the Dispose method of its parent type. The Dispose method of the parent type should release all the resources that it owns and in turn call the Dispose method of its parent type.

A Dispose method calls the GC.SuppressFinalize method for the object that it is disposing. If an object is currently on the finalization queue, GC.SuppressFinalize prevents the Finalize method from being called. Executing the Finalize method affects performance. Therefore, if the Dispose method has already done the work to clean up an object, then it is not necessary for the garbage collector to call the Finalize method of the object.

The BaseResource class of the .NET Framework implements the IDisposable interface and defines a public Dispose method. The cleanup code for the object is executed in this Dispose method. The Dispose method takes either a true or a false as an argument depending on the identity of the caller. If the argument is true, it means that the method has been called by the code; hence, all the managed and unmanaged resources can be disposed. If the argument is false, it means that the method has been called from the runtime; hence, only the unmanaged resources can be disposed. The BaseResource class also provides a destructor as a safeguard mechanism in case the Dispose method is not called.

The following example code illustrates a design pattern for implementing the Dispose method for classes that encapsulate unmanaged resources. This pattern is implemented throughout the .NET Framework.

In this example, the class MyResourceWrapper illustrates how to derive from a class that implements resource management using the Dispose method. MyResourceWrapper overrides the virtual Dispose(bool) method and provides clean-up code for the managed and unmanaged resources that it creates. MyResourceWrapper also calls the Dispose method on its base class, BaseResource, to ensure that its base is properly cleaned up.

.NET example: Code for implementing the Dispose method

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

using System;
using System.ComponentModel;
// Design pattern for the base class.
// By implementing IDisposable, you are announcing
// that instances of this type allocate scarce resources.
public class BaseResource: IDisposable
{
   // Pointer to an external unmanaged resource.
   private IntPtr handle;
   // Other managed resource this class uses.
   private Component Components;
   // Track whether Dispose has been called.
   private bool disposed = false;
   // Constructor for the BaseResource object.
   public BaseResource()
   {
      // Insert appropriate constructor code here.
   }
   // Implement IDisposable.
   // Do not make this method virtual.
   // A derived class should not be able to override
   // this method.
   public void Dispose()
   {
      Dispose(true);
      // Take yourself off the Finalization queue 
      // to prevent finalization code for this object
      // from executing a second time.
      GC.SuppressFinalize(this);
   }
   // Dispose(bool disposing) executes in two distinct
   // scenarios.
   // If disposing equals true, the method has been 
   // called directly
   // or indirectly by a user's code. Managed and 
   // unmanaged resources
   // can be disposed.
   // If disposing equals false, the method has been
   // called by the 
   // runtime from inside the finalizer and you should
   // not reference 
   // other objects. Only unmanaged resources can be 
   // disposed.
   protected virtual void Dispose(bool disposing)
   {
      // Check to see if Dispose has already been called.
      if(!this.disposed)
      {
         // If disposing equals true, dispose all managed 
         // and unmanaged resources.
         if(disposing)
         {
            // Dispose managed resources.
            Components.Dispose();
         }
         // Release unmanaged resources. If disposing is false, 
         // only the following code is executed.
         CloseHandle(handle);
         handle = IntPtr.Zero;
         // Note that this is not thread safe.
         // Another thread could start disposing the object
         // after the managed resources are disposed,
         // but before the disposed flag is set to true.
         // If thread safety is necessary, it must be
         // implemented by the client.
      }
      disposed = true;         
   }
   // Use C# destructor syntax for finalization code.
   // This destructor will run only if the Dispose method 
   // does not get called.
   // It gives your base class the opportunity to finalize.
   // Do not provide destructors in types derived from
   // this class.
   ~BaseResource()      
   {
      // Do not recreate Dispose clean-up code here.
      // Calling Dispose(false) is optimal in terms of
      // readability and maintainability.
      Dispose(false);
   }
   // Allow your Dispose method to be called multiple times,
   // but throw an exception if the object has been disposed.
   // Whenever you do something with this class, 
   // check to see if it has been disposed.
   public void DoSomething()
   {
      if(this.disposed)
      {
         throw new ObjectDisposedException();
      }
   }
public void CloseHandle(IntPtr h)
    {
        //cleanup of handle
        // Write code here to cleanup your unmanaged resource
    }
    public void CloseHandle(NativeResource n)
    {
        CloseHandle(n.handle);
    }
}
public class ManagedResource:BaseResource
{
};
public class NativeResource:BaseResource
{
};
// Design pattern for a derived class.
// Note that this derived class inherently implements the 
// IDisposable interface because it is implemented in
// the base class.
public class MyResourceWrapper: BaseResource
{
   // A managed resource that you add in this derived
   // class.
    private ManagedResource addedManaged = new 
ManagedResource();
// A native unmanaged resource that you add in this
// derived class.
    private NativeResource addedNative = new 
NativeResource();
   private bool disposed = false;
  // Constructor for this object.
   public MyResourceWrapper()
   {
      // Insert appropriate constructor code here.
   }
   protected override void Dispose(bool disposing)
   {
      if(!this.disposed)
      {
         try
         {
            if(disposing)
            {
               // Release the managed resources you added in
               // this derived class here.
               addedManaged.Dispose();         
            }
            // Release the native unmanaged resources you added
            // in this derived class here.
            CloseHandle(addedNative);
            this.disposed = true;
         }
         finally
         {
            // Call Dispose on your base class.
            base.Dispose(disposing);
         }
      }
   }
}
// This derived class does not have a Finalize method
// or a Dispose method without parameters because it
// inherits 
// them from the base class.

(Source File: N_MemMgt-UAMV4C5.01.cs)

Thread Local Storage

The Thread Local Storage (TLS) mechanism enables storing of data in a thread and accessing the data anywhere the thread exists. The System.Threading namespace allows developers to use TLS within their multithreaded applications.

Whenever a process is created, the CLR allocates a multislot data store array to each and every process. Threads, with the help of the flexible access methods, can use these data slots within the data stores to store and retrieve information that is unique to a thread and an application. There are two types of data slots: named slots and unnamed slots. The named slots can use a mnemonic identifier. However, other components can, intentionally or unintentionally, modify them by using the same name for their own thread-relative storage. However, if an unnamed data slot is not exposed to other code, it cannot be used by any other component. To use managed TLS, create a data slot using Thread.AllocateNamedDataSlot or Thread.AllocateDataSlot, and use the appropriate methods to set or retrieve the information placed there.

The following is an example of a console application in .NET illustrating the use of TLS for storing information specific to a thread. A dispatcher object is created, which keeps calling the ProcessSignal() method of the receiver in a loop. The receiver object generates a random number for every call from the dispatcher and writes this number on the TLS, which the dispatcher can read.

.NET example: Using Thread Local Storage

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

using System;
using System.Threading;
namespace TLSExample
{
// This Console Application is to demonstrate how TLS 
// can be used to store information specific to a thread
class TLSExample
{
    public static void Main(string[] args)
    {
        // Allocate a named data slot on all threads
        Thread.AllocateNamedDataSlot ("receivervalue");
               Despatcher desOb =new Despatcher ();
        // Thread out to the other classes
        ThreadStart myThreadStart = new ThreadStart 
(desOb.DespatchWork);
        Thread myThread = new Thread(myThreadStart);
           // Start the thread here.
        myThread.Start();    
          // free the memory on all threads
        Thread.FreeNamedDataSlot("receivervalue");
    }
}
public class Despatcher
{
    // This is class implementation that despatches down
    // into other classes
          private Receiver recOb;
    public Despatcher()
    {
        recOb = new Receiver();
    }
         public void DespatchWork()
    {
        // Looping to simulate processing        
        for(int i=0;i<50;i++)
        {            
        // The Despatcher despatches to receiver by calling 
        // the processSignal method of the Receiver
        recOb.ProcessSignal();        
        //After processing by the Receiver, the calling 
        //class needs to access the information on TLS
        int valueFromReceiver = DetermineVFR();
Console.WriteLine ("The value pulled from TLS " + 
valueFromReceiver);
        }
    }
        public int DetermineVFR()
    {
        LocalDataStoreSlot vfcTLS;
        vfcTLS = Thread.GetNamedDataSlot("receivervalue");
        int vfc = (int) Thread.GetData(vfcTLS);
        return(vfc);
    }
}
public class Receiver
{
// This class processes work from the despatcher and 
// returns back to despatcher with information on thread
    private Random ranTime;
    private int max;
    public Receiver()
    {
        ranTime = new Random();
        max = 500;
    }
    public bool ProcessSignal()
    {
    // Generate some random number to store different
    // values on all threads
    int rndValue=ranTime.Next(max);
    LocalDataStoreSlot myData;
    myData = Thread.GetNamedDataSlot("receivervalue");
    // Set the named data slot equal to the random number
    // created above
    Thread.SetData(myData,rndValue);        
    return true;
     }
}
}

(Source File: N_MemMgt-UAMV4C5.02.cs)

File Management

This section details the file management techniques in the UNIX and .NET environments. The following topics are explained in detail:

  • File access mechanisms

  • File open and access modes

  • Migrating using interoperability strategies

  • Working with directories

Every program that runs from the UNIX shell opens three standard files: standard input, standard output, and standard error. These files have the integer file descriptors and provide the primary means of communication between the programs. These file descriptors are 0, 1, and 2 respectively for standard input, standard output, and standard error. These files exist as long as the process runs. UNIX provides two kinds of file access: low-level file access and standard, or stream, file access.

File Access Mechanisms

This section describes the various file access mechanisms using low-level file input/output routines and the stream file access routines on the UNIX and .NET environments.

UNIX File Access

UNIX file access is mainly classified as two types: low-level file access and stream file access.

Low-Level File Access

The low-level input/output (I/O) functions invoke the operating system more directly for low-level operations than that provided by standard (or stream) I/O. Function calls relating to low-level input and output do not buffer or format data. They deal with bytes of information, which means that you are using the binary files instead of the text files. The low-level file handles or file descriptors, which give a unique integer number to identify each file, are used instead of file pointers.

Stream File Access

The standard, or stream, I/O functions process data in different sizes and formats, ranging from a single character to large data structures. They also provide buffering, which can improve performance. Using the stream file access functions, you can open a file either in the binary mode or in the text mode. In the binary mode, a program can access every byte in the file, whereas the text mode is normally used for text files in which some characters may be "hidden" from the program.

.NET File Access

In .NET, the System::IO namespace provides a FileStream method to access the contents of a file. If a low-level file access or the binary mode in a stream file access is used in UNIX, then consider using the BinaryReader and BinaryWriter classes in .NET. These .NET classes enable you to read from and write into binary files. The System::IO namespace also provides the StreamReader and StreamWriter classes for processing text files, which can be used for migrating the nonbinary mode file access code.

File Access Through the FileStream Class

The FileStream class provides access to files, including the standard input, output, and error devices. Use the FileStream class to read from, write to, open, and close files on a file system, as well as to manipulate other file-related operating system handles such as pipes, standard input, and standard output. The FileStream class also buffers input and output for better performance.

There are different types of FileStream constructors that you can use depending on your requirement. One common usage is as follows.

FileStream (“File name”, FileMode, FileAccess)

You can also use the FileInfo class, which helps in the creation of FileStream objects.

Note More information on the usage of the FileStream class in the System.IO namespace is available at https://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemIOFileStreamClassTopic.asp.

File Access Through BinaryReader and BinaryWriter

The BinaryReader class is used for reading strings and elementary data types as binary values, whereas the BinaryWriter class is used for writing elementary types in binary data. When writing to files, an application may have to create a file if the file to which the application is trying to write does not exist. To do so, the application requires permission for the directory in which the file is to be created. However, if the file already exists, the application only requires write permission to the file itself. Wherever possible, it is more secure to create the file during deployment and only grant write permission to that file instead of granting permission to the entire directory. It is also more secure to write data to the user directories than to the root directory or the Program Files directory.

File Access Through StreamReader and StreamWriter

The System.IO namespace class allows you to read and write characters to and from files as streams, or contiguous groups of data, using specific encoding to convert characters to and from bytes. It includes the StreamReader and StreamWriter classes, which enable you to read or write a sequential stream of characters to or from a file. The StreamReader and StreamWriter classes mirror the functionality of the BinaryReader and BinaryWriter classes, but they read and write information as text rather than as binary data.

The following code sample reads up to 1 KB of characters from the input file and writes that information to the output file. If any input/output errors occur, an error message is output to the standard error file descriptor.

UNIX example: Reading and writing files using low-level functions

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    char block[1024]; 
    int in, out; 
    int num_read;
    in = open("input_file", O_RDONLY); 
    if (in == -1) {
        write(2, "An error has occurred opening 
the file: ‘input_file’\n", 52);
        exit(1);
    }
    out = open("output_file", O_WRONLY|O_CREAT, S_IRUSR
|S_IWUSR);
    if (out == -1) {
        write(2, "An error has occurred opening the
file: ‘output_file’\n", 53);
        exit(1);
    }
    while((num_read = read(in,block,sizeof(block))) > 0)
        write(out, block, num_read);
    exit(0);
}

(Source File: U_FileMgt-UAMV4C5.01.c)

The Managed C++ code sample reads up to 1 KB of characters from the input file and writes them to the output file.

.NET example: Reading and writing files using BinaryReader and BinaryWriter

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
#define BUF_SIZE 1024
using namespace System;
using namespace System::IO;
int main(int argc, char *argv[])
{
  try
  {
    Byte block[] = new Byte[BUF_SIZE];
    FileStream *in = 
        new FileStream("input_file", FileMode::Open,
FileAccess::Read);
    FileStream *out = 
        new FileStream("output_file", FileMode::
Create, FileAccess::ReadWrite);
    BinaryReader *source = new BinaryReader(in);
    BinaryWriter *dest = new BinaryWriter(out);
    while (int numBytes = source->Read(block, 0, BUF_SIZE))
    {
      dest->Write(block, 0, numBytes);
    }
    source->Close();
    dest->Close();
    in->Close();
    out->Close();
  }
  catch (Exception *e)
  {
    Console::WriteLine(e->get_Message());
  }
}

(Source File: N_FileMgt-UAMV4C5.01.cpp)

The following code sample reads characters from the input file opened with the standard file I/O library function fopen() and writes that information to the output file, also opened with fopen(). Then it uses a loop of the fgetc and fputc calls to transfer the contents of "input_file" to "output_file".

UNIX example: Copying a file using stream file functions

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

			
#include <stdio.h>
int main()
{
    int c;
    FILE *in, *out;
    in = fopen("input_file","r");
    if (in == NULL) {
        write(2, "An error has occurred opening the
file: ‘input_file’\n", 52);
        exit(1);
    }
    out = fopen("output_file","w");
    if (out == NULL) {
        write(2, "An error has occurred opening the
file: ‘output_file’\n", 53);
        exit(1);
    }
    while((c = fgetc(in)) != EOF)
        fputc(c,out);
    fclose(in);
    fclose(out);
    exit(0);
}

(Source File: U_FileMgt-UAMV4C5.02.c)

.NET provides a simpler way of copying the contents of one file into another. The following code sample uses the ReadToEnd() method of the StreamReader class to read the contents of the input file to a string and later writes the read string to the output file.

.NET example: Copying files using StreamReader and StreamWriter

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::IO;
int main()
{
  try
  {
    FileStream *in = 
        new FileStream("input_file", FileMode::Open,
FileAccess::Read);
    FileStream *out = 
        new FileStream("output_file", FileMode::Create
, FileAccess::ReadWrite);
    StreamReader *source = new StreamReader(in);
    StreamWriter *dest = new StreamWriter(out);
    String *str = source->ReadToEnd();
    dest->Write(str);
    source->Close();
    dest->Close();
    in->Close();
    out->Close();
  }
  catch (Exception *e)
  {
    Console::WriteLine(e->get_Message());
  }
}

(Source File: N_FileMgt-UAMV4C5.02.cpp)

File Open and Access Modes

Table 5.1 lists the file access modes used in the fopen() statement in UNIX and its equivalent in the .NET Framework.

Table 5.1. Mapping of File Modes in fopen() to System::IO Namespace

File Modes in fopen()

File Modes in  System::IO

Operation

r

FileMode::Open

FileAccess:Read

Opens a text file for reading.

w

FileMode::OpenOrCreate

FileAccess:Write

Opens a text file for writing. If the file does not exist, a new file is created.

a

FileMode::Append

FileAccess::Write

Opens a text file for appending. The text that is written to is added at the end of the file. If the file does not exist, a new file is created.

rb

FileMode::Open

FileAccess::Read

Use the BinaryReader class for reading.

Opens a binary file for reading.

wb

FileMode::OpenOrCreate

FileAccess::Write

Use the BinaryWriter class for writing.

Opens a binary file for writing. If the file does not exist, a new file is created.

ab

FileMode::Append

FileAccess::Write

Use the BinaryWriter class for writing.

Opens a binary file for appending. If the file does not exist, a new file is created.

r+

FileMode::Open

FileAccess::ReadWrite

Opens a text file for reading and writing.

w+

FileMode::OpenOrCreate

FileAccess::ReadWrite

Creates a new text file for reading and writing. If a file with the same name already exists, it is over-written. If the file does exist, a new file is created.

a+

Append access can be requested only in write mode.

To write use the following:

FileMode::Append

FileAccess::Write

Opens a text file for reading and appending.

r+b

FileMode::Open

FileAccess::ReadWrite

Use the BinaryReader and BinaryWriter classes for reading and writing.

Opens a binary file for reading and writing.

w+b

FileMode::OpenOrCreate

FileAccess::ReadWrite

Use the BinaryReader and BinaryWriter classes for reading and writing.

Creates a binary file for reading and writing. If a file with the same name already exists, it is overwritten.

a+b

Append access can be requested only in write mode.

Use the following modes:

FileMode::Append

FileAccess::ReadWrite

Use the BinaryReader and BinaryWriter classes for reading and writing.

Opens a binary file for reading and appending.

Migrating Using Interoperability Strategies

File I/O calls are provided by the standard I/O library stdio.h. This library is a part of ANSI standard C; hence it ports directly to Microsoft Windows. Similarly, many C and C++ file-related programs on UNIX can be recompiled on Windows with minimal changes. These programs can also be directly recompiled in a .NET Managed C++ project using the /CLR compiler switch (IJW mechanism).

You can also use the other .NET interoperability strategies, such as wrapping the unmanaged classes, or Platform Invocation services (DllImport) to migrate the file-related code to .NET. However, as explained in the “.NET Interoperability Mechanisms” section in Chapter 3, “.NET Interoperability” of this volume, the file-operations code will run unmanaged in the .NET environment.

Working with Directories

This section describes various routines to perform actions related to directories in UNIX and .NET such as accessing the current working directory, changing a directory, and deleting a directory.

Accessing the Current Working Directory

Directory operations involve calling the appropriate functions to traverse a directory hierarchy and to list the contents of a directory. The Directory class of the System.IO namespace in .NET contains all such appropriate directory access and manipulation functions.

UNIX provides the _getcwd(), _get_current_dir_name(), and _getwd() functions to get the current working directory.

#include <unistd.h>
char *getcwd(char *buf, size_t size);
char *get_current_dir_name(void);
char *getwd(char *buf);

This code sample prints out the current working directory using the get_current_dir_name() function.

UNIX example: Print the current working directory

#include <unistd.h>
#include <stdio.h>
int main()
{
    char *cwd;
    cwd = (char *)get_current_dir_name();
    printf("Current working directory: %s", cwd);
    exit(0);
}

(Source File: U_WorkingWithDir-UAMV4C5.01.c)

The Directory class in the System.IO namespace provides a static method called GetCurrentDirectory() to print the current directory. The following Managed C++ code sample prints out the current working directory using the GetCurrentDirectory() method.

.NET example: Print the current working directory

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::IO;
 int main()
 {
   try
     {
       String* cwd; 
       cwd = Directory::GetCurrentDirectory();
       Console::WriteLine(S"Current working directory:
{0}",cwd);
     }
   catch (Exception *e)
   {
    Console::WriteLine(S"The process failed:{0}",e);
   }
  }

(Source File: N_WorkingWithDir-UAMV4C5.01.cpp)

Most of the methods in the Directory class provide an object-oriented mechanism to access the underlying Microsoft Win32® application programming interface (API). For example, the Directory::GetCurrentDirectory() internally calls the Win32 API function GetCurrentDirectory(), which has been discussed in Volume 3: Migrate Using Win32/Win64 of this guide. Similarly, the Directory class has a method called GetLogicalDrives (), which returns all available drives in the system as an array of strings. Internally, this method calls kernel32.dll GetLogicalDrives(), which is a file I/O function in the kernel32.dll library.

Similarly, all Directory class methods internally invoke the Win32 native functions. In addition, all methods in the Directory class are static and need not be instantiated.

Accessing Directories

UNIX provides two system—opendir() and readdir()—to open and display the contents of a UNIX directory. The following code example in UNIX takes the name of the directory from the command line and displays its contents using the earlier system calls.

UNIX example: Accessing directories

#include <dirent.h>
#include <stdio.h>
#include <iostream>
void main(int argc, char *argv[])
{
  struct dirent *entryp;
  DIR *dp;
  char *directory = argv[1];
  if ((dp = opendir(directory)) == NULL)
  {
    perror("opendir failed");
    exit(1);
  }
  while ((entryp = readdir(dp)) != NULL)
    cout << entryp->d_name << endl;
  exit(0);
}

(Source File: U_WorkingWithDir-UAMV4C5.02.c)

The same operation can be achieved in .NET by using the Directory class and the GetFiles() method. The following Managed C++ code example shows how the earlier UNIX example is migrated to .NET.

.NET example: Accessing directories

#using <mscorlib.dll>
using namespace System;
using namespace System::IO;
void _tmain(int argc, char *argv[])
{
  try
  {
       String *fileNames[];
    String *directoryName = argv[1];
    fileNames = Directory::GetFiles(directoryName);
    for (int i = 0; i < fileNames->Length; i++)
      Console::WriteLine(fileNames[i]);
  }
  catch (Exception *e)
  {
    Console::WriteLine(e->get_Message());
  }
}

(Source File: N_WorkingWithDir-UAMV4C5.02.cpp)

Other Directory Operations

The following examples show how some common directory operations, such as creating a directory, changing the current directory, and deleting a directory, are performed using the System::IO namespace of .NET.

.NET example: Creating a new directory

The following Managed C++ creates a new directory called Test.

Note: Some of the lines in the following code have been displayed on multiple lines for better readability.

#using <mscorlib.dll>
using namespace System;
using namespace System::IO;
int main()
 {
   try
     {
    String* newDir = S"Test";
    Directory::CreateDirectory (newDir);
    Console::WriteLine (S"New Directory created");
     } catch(Exception *e){ Console::WriteLine(S"The
process failed:{0}",e); }
}

(Source File: N_WorkingWithDir-UAMV4C5.03.cpp)

UNIX example: Changing the current directory

The following code example in UNIX changes the current directory to a directory called Test if such a directory exists within the current directory. It uses the function chgdir() for this purpose.

#include <unistd.h>
#include <stdio.h>
int main() 
{
    char chgDir[] = "Test";
    int res;
    res = chdir(chgDir);
    if(res == 0)
    {
        printf("Change Directory successful");
    }
    else
    {
        printf("Error in Change Directory");
        exit(1);
    }    
}

(Source File: U_WorkingWithDir-UAMV4C5.04.c)

.NET example: Changing the current directory

The following Managed C++ sample code changes the current directory to a directory called Test if such a directory exists within the current directory.

#using <mscorlib.dll>
using namespace System;
using namespace System::IO;
int main()
 {
   try
     {
    String* chgDir = "Test";
    if(Directory::Exists(chgDir))
    {
        Directory::SetCurrentDirectory(chgDir);
        Console::WriteLine(S"The Directory changed");
    }
    else
    {
        Console::WriteLine(S"The Directory does not exist");
    }
     } catch(Exception *e){Console::WriteLine(S"The
process failed:{0}",e); }
   }

(Source File: N_WorkingWithDir-UAMV4C5.04.cpp)

UNIX example: Deleting a directory

The following code example in UNIX deletes a directory named Test if such a directory exists within the current directory. It uses the remove() function for this purpose.

#include <unistd.h>
#include <stdio.h>
int main() 
{
    char delDir[] = "Test";
    int res;
    res = remove(delDir);
    if(res == 0)
    {
        printf("The directory successfully deleted");
    }
    else
    {
        printf("Unable to delete the directory");
    }  
}

(Source File: U_WorkingWithDir-UAMV4C5.05.c)

.NET example: Deleting a directory

The following Managed C++ code example deletes a directory named Test if such a directory exists within the current directory.

#using <mscorlib.dll>
using namespace System;
using namespace System::IO;
int main() {
    // Specify the directories you want to manipulate.
    String* delDir = S"Test";
    try {
        // Determine whether the directory exists.
        if (Directory::Exists(delDir)) 
{
        Directory::Delete(delDir);
        Console::WriteLine(S"The Directory Deleted");
        }
    else 
    {
        Console::WriteLine(S"Directory does not exist");
    }
    } catch (Exception* e) {
        Console::WriteLine(S"The process failed: {0}", e);
    } 
}

Download

Get the UNIX Custom Application Migration Guide

Update Notifications

Sign up to learn about updates and new releases

Feedback

Send us your comments or suggestions