New version of this article
For anyone who may be interested, I posted an update version of this article here. :)
Introduction
"INI" files are widely used throughout the Windows operating system. They were there before the Registry came around and are still there today.
They are text files, usually (but not necessarily) with a ".ini" extension, and are devoted to storing various kind of settings.
Their structure is rather simple: settings are organized in "sections", with section headers identified by surrounding square brackets ("[" "]"). Each section contains one or more pairs of "key/value" strings. Each key is unique within a section, and identifies a specific setting.
An example of INI file content is:
[Section1]
Key1=somevalue
Key2=somevalue
...
[Section2]
Key1=somevalue
Key10=somevalue
...
Using INI files can be a convenient way to store your application settings, since the file format is easy to read, understand and modify simply by using a text editor.
Background
Using INI files in a Windows application is usually achieved by means of some API calls exposed by the "kernel32.dll" library. This can be done in C# by using the "DllImport
" directive:
#region "WIN32API"
[DllImport("kernel32.dll")] private static extern int GetPrivateProfileInt
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,int nDefault,
[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
byte[] lpReturnedString,int nSize,[MarshalAs(UnmanagedType.LPStr)]
string lpFileName);
[DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
[MarshalAs(UnmanagedType.LPStr)] string lpReturnedString,
int nSize,[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
[DllImport("kernel32.dll")] private static extern int WritePrivateProfileString
([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
[MarshalAs(UnmanagedType.LPStr)] string lpString,
[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
#endregion
I decided it would be fun to write my own cross-platform class to easily make use of INI files under both Mono and .NET.
The result is the INIFile
class, which is compatible with standard Windows INI files, with a few exceptions:
- Section and key names are case sensitive.
- It uses caching to boost performance.
- When changing or adding values, it does not preserve the original INI file structure.
Caching is performed by means of Dictionaries which hold in memory all sections and key/value pairs.
Using the Code
The INI file is read and parsed upon creation of an instance of the class, and its content is stored in the cache. The INI file name (complete path to the desired file) is passed to the constructor.
INIFile MyINIFile = new INIFile("myinifilename.ini");
The user program can then query the content of the cache by calling the GetValue()
method, supplying a section and a key name.
string Value = MyINIFile.GetValue("Section", "Key", "Default value");
To keep compatibility with the behaviour of the Windows API calls, no exception is thrown if the INI file does not exist or if an inexisting section or key is requested. Instead, the default value supplied by the user will be returned. This allows to easily manage situations where settings are optionally modified by the end user and default values are hard-coded in the software.
There is also an option for lazy loading of the INI file content (simply pass true
as second parameter to the constructor call), which tells the class instance to load the INI file content only at the first query or value modification. This can be useful to speed up application startup when necessary.
INIFile MyINIFile = new INIFile("myinifilename.ini", true);
Value can be changed by calling the SetValue()
method.
MyINIFile.SetValue("Section", "Key", "Value");
If the supplied section or key are inexisting, they will be automatically added to the cache. Saving all changes back to the INI file is done by explicitly calling the Flush()
method.
MyINIFile.Flush();
If any modification is present in the cache, all sections and key/value pairs will be written back to disk, overwriting the old file. If the file is not existing, it will be created. It is up to the user program to ensure integrity of the file contents by explicitly avoiding situations where two different instances of the INIFile
class point to the same file. It's also interesting to note that any changes made by means of an external text editor after the INI file was loaded to the local cache will be lost after a call to Flush()
(but only if a modification is present in the cache).
It is possible to force a refresh of the cache by calling the Refresh()
method. The INI file will be parsed again and the content of the cache will be replaced by the file content. Any modification made to the cache after the last call to Flush()
will be lost.
MyINIFile.Refresh();
In most typical situations, the best way to manage INI files is to create an instance of the INIFile
class every time you need to read or write your application settings, and then destroy it. This will ensure that any changes made by means of a text editor will not be overwritten.
For example, you can create an instance of the class during the application startup to read the settings and store them, and then create another instance to write changes to the settings when the application closes, or when the user presses the "Ok" button on your "change settings" dialog, and so on.
Since all values are string
s, the user program can easily encode any desired data type to a string
in order to write it to the INI file, and decode it accordingly upon reading the same file. The INIFile
class, however, offers a few getters and setters to automatically manage some of the basic data types, in order to cover all typical needs.
internal string GetValue(string SectionName, string Key, string DefaultValue)
internal bool GetValue(string SectionName, string Key, bool DefaultValue)
internal int GetValue(string SectionName, string Key, int DefaultValue)
internal double GetValue(string SectionName, string Key, double DefaultValue)
internal byte[] GetValue(string SectionName, string Key, byte[] DefaultValue)
internal void SetValue(string SectionName, string Key, string Value)
internal void SetValue(string SectionName, string Key, bool Value)
internal void SetValue(string SectionName, string Key, int Value)
internal void SetValue(string SectionName, string Key, double Value)
internal void SetValue(string SectionName, string Key, byte[] Value)
int
s and double
s are simply converted to string
s (using culture-neutral settings, so the file will be cross-culture compatible), while bool
s will be converted to integer values, where 0
means false
and any other value means true
. This makes it easy for the user to modify the values with a text editor.
There is also the possibility to store a byte
array. This is extremely useful when storing hashes, uuids or any other kind of custom binary data. The framework offers methods for converting string
s and other data to and from byte arrays, thus making it extremely easy to use this feature. Byte arrays are converted to hexadecimal string
s when written to the INI file, which makes it possible to modify their values with a text editor.
A Note on Thread-safety and Multi-process Access
The class includes a basic locking system for thread-safety. All calls to the class methods are thread-safe.
Locking is done on accesses to the file and cache, and the same lock is used for both in order to guarantee the maximum possible data integrity in multi-threaded applications.
That said, you must remember that it's up to the user code to ensure full data integrity, since locking is done separately for each method call! So for example, having two different threads performing a few calls to SetValue()
and then Flush()
on the same instance of the class WILL require explicit locking by the user in order to prevent data loss. If the threads use different instances of the class to point to the same file, it's even more important to use locking to prevent not only data loss but also exceptions upon file access.
The same applies to accessing the file from different processes: you must either set up an interprocess locking system or be sure to manage all possible concurrency problems.
Source and Sample Code
You can download the class source code together with some sample code by means of the link in the upper part of this page.
It was developed in Mono 2.0 using MonoDevelop 2.0 Alpha 1, but is perfectly compatible with .NET - simply cut and paste the code into a Visual Studio project or just compile the source code with the .NET compiler.
Here is an extract of the sample code, which shows how to use the class features:
StreamReader sr = null;
try
{
Console.WriteLine("Creating INIFile object for \"test.ini\"...");
INIFile MyINIFile = new INIFile("test.ini");
Console.WriteLine("\nGetting values...\n");
int Value1 = MyINIFile.GetValue("Section1","Value1",0);
bool Value2 = MyINIFile.GetValue("Section1","Value2",false);
double Value3 = MyINIFile.GetValue("Section1","Value3",(double)0);
byte[] Value4 = MyINIFile.GetValue("Section1","Value4",(byte[])null);
Console.Write("(int) Value1=");
Console.WriteLine(Value1.ToString());
Console.Write("(bool) Value2=");
Console.WriteLine(Value2.ToString());
Console.Write("(double) Value3=");
Console.WriteLine(Value3.ToString());
Console.Write("(byte[]) Value4=");
Console.WriteLine(PrintByteArray(Value4));
Console.WriteLine("\nSetting values...\n");
Value1++;
Value2 = !Value2;
Value3 += 0.75;
Value4 = new byte[] { 10, 20, 30, 40 };
MyINIFile.SetValue("Section1","Value1", Value1);
MyINIFile.SetValue("Section1","Value2", Value2);
MyINIFile.SetValue("Section1","Value3", Value3);
MyINIFile.SetValue("Section1","Value4", Value4);
Console.Write("(int) Value1=");
Console.WriteLine(Value1.ToString());
Console.Write("(bool) Value2=");
Console.WriteLine(Value2.ToString());
Console.Write("(double) Value3=");
Console.WriteLine(Value3.ToString());
Console.Write("(byte[]) Value4=");
Console.WriteLine(PrintByteArray(Value4));
Console.WriteLine("\nFlushing cache...");
MyINIFile.Flush();
Console.WriteLine("\nFile content:\n");
sr = new StreamReader("test.ini");
string s;
while ((s=sr.ReadLine()) != null) Console.WriteLine(s);
Console.WriteLine("\nDone.");
}
catch (Exception ex)
{
Console.Write("\n\nEXCEPTION: ");
Console.WriteLine(ex.Message);
}
finally
{
if (sr != null) sr.Close();
sr = null;
}
This is the printout of the results for running the example for the first time:
Creating INIFile object for "test.ini"...
Getting values...
(int) Value1=0
(bool) Value2=False
(double) Value3=0
(byte[]) Value4=
Setting values...
(int) Value1=1
(bool) Value2=True
(double) Value3=0,75
(byte[]) Value4=10, 20, 30, 40
Flushing cache...
File content:
[Section1]
Value1=1
Value2=1
Value3=0.75
Value4=0a141e28
Done.
Points of Interest
The class can be improved in many ways: implementing a better caching system, automatic flushing upon disposal (although I personally don't like this solution, so I abandoned the idea), writing only modified values to the file (loss of performance but in some cases a better way to ensure data integrity), a way to automatically encode any kind of data by using generics and wrapper classes, or even better a standard way to dump and reload classes (using reflection) to the INI file, mapping them to the section/key structure, and so on.
The basics are all there, and it should be easy enough to add flavour to the class. Enjoy! :)
History
- 2009-04-13 - V1.0 - First release