Introduction
Asterisk is a complete open source PBX software, originally written by Marl Spencer of Digium, Inc., and tested and improved by open-source coders around the world. The Inter-Asterisk eXchange (IAX) protocol, used in Asterisk, enables VoIP connections between Asterisk servers and clients.
To interact with the IAX protocol, you can use a C++ portable client library called IAXClient that enables anyone who wants to communicate to Asterisk servers calling exported functions. These functions handle the telephony operations including call handling, network protocols, and audio encoding/decoding. The library is designed to be built in multiple platforms using CygWin shell and, to use it in Win32, you must create a DLL file using the GCC compiler and some CygWin tools. In this article, I'll show:
- The main functions and structures used in IAXClient.
- How to use it in C# using P/Invoke.
- How to compile it.
IAXFunctions class
To use IAXClient functions in C#, we need to use the Platform Invoke (P/Invoke) service. This service enables managed code calls to unmanaged functions in DLLs, and in C#, we need to create a static class that holds the main IAXClient functions.
Simple parameters
The IAXFunction
class has the most important functions converted to C# types. Some functions have primitive types, and can be converted directly. For example, the function iaxc_initialize
accepts two integer parameters and returns an integer value:
EXPORT int iaxc_initialize(int audType, int nCalls);
In C#, we use the [DLLImport]
attribute (DllImportName
is a constant having the DLL path):
[DllImport(DllImportName, CallingConvention = CallingConvention.StdCall)]
public static extern int iaxc_initialize(int audType, int calls);
Other functions have character parameters that must be converted to C# types. For example, the function iaxc_register
accepts two char
pointer parameters and returns an integer value:
EXPORT int iaxc_register(char *user, char *pass, char *host);
In C#, this kind of a parameter can be converted to string
but you need to set the MarshalAs
attribute:
[DllImport(DllImportName)]
public static extern int iaxc_register(
[MarshalAs(UnmanagedType.LPStr)] string user,
[MarshalAs(UnmanagedType.LPStr)] string pass,
[MarshalAs(UnmanagedType.LPStr)] string host);
The MarshalAs
tells C# how to marshal the data to the unmanaged function, and you need to set it according to the parameter type. In this case, the UnmanagedType
enumeration used is LPStr
, indicating that we have a null-terminated ANSI character string from unmanaged code.
Struct parameters
You can pass and receive structure parameters using the MarshalAs
attribute with the UnmanagedType.LPStruct
enumeration, but in the function iaxc_audio_devices_get
, we need special care. Look at the function signature:
EXPORT int iaxc_audio_devices_get(struct iaxc_audio_device **devs,
int *nDevs, int *input, int *output, int *ring);
The function needs a pointer to a pointer parameter (indicated by **
). C# doesn't have a UnmanagedType
option to use with this kind of a parameter, and we need a workaround here:
[DllImport(DllImportName, CallingConvention = CallingConvention.StdCall)]
public static extern int iaxc_audio_devices_get(
ref IntPtr devs,
ref int nDevs,
ref int input,
ref int output,
ref int ring);
The IntPtr
type is a specific type that represents a pointer, and we can use it to represent the 'a pointer' part of the parameter. The first part, 'a pointer to' can be achieved using the ref
parameter attribute, specifying that the parameter is a reference parameter used normally in C# code. So, ref IntPtr
means a pointer (ref
) to a pointer (IntPtr
).
MarshalEx class
Even using the ref IntPtr
, we cannot access the structure values because the IntPtr
has only a handle to it. To access it, we need to use the PtrToStructure
method from the .NET Marshal
class. This method takes an IntPtr
parameter and a structure type parameter, and returns (casts) the structure object:
IntPtr pdevs = IntPtr.Zero;
int nDevs = 0;
int input = 0;
int output = 0;
int ring = 0;
if (IAXFunctions.iaxc_audio_devices_get(ref pdevs,
ref nDevs, ref input, ref output, ref ring) != 0)
throw new IAXMethodException("Initialize error!");
iaxc_audio_device devs = (iaxc_audio_device)
Marshal.PtrToStructure(pdevs, typeof(iaxc_audio_device));
In this code, we initialize the IntPtr
parameters with IntPtr.Zero
and call the function. The pdevs
parameter has the pointer to the iaxc_audio_device
value, and using the PtrToStructure
method, we can cast the pointer to the structure. So far so good.
But, the function has a nDevs
parameter that indicates the number of devices pointed in the pdevs
parameter. If the nDevs
parameter returns 2, the pdevs
parameter points to two iaxc_audio_device
structures. Remember that the pdevs
parameter is a pointer to a pointer, and in this case, it's pointing to the first structure. The next structures returned are pointed using an array (something like iaxc_audio_device[]
).
To loop through all the devices, we need to use the MarshalEx
class, calling the PtrToStructureArray
method that walks in the pdevs
parameter:
public class MarshalEx
{
public static object[] PtrToStructureArray(IntPtr pointer,
Type structureType, int len)
{
object[] array = new object[len];
for (int i = 0; i < len; i++)
{
array[i] = Marshal.PtrToStructure(pointer, structureType);
pointer = (IntPtr) (pointer.ToInt32() + Marshal.SizeOf(array[i]));
}
return array;
}
}
The trick here is to walk in the pointer
parameter address memory using the size of the structure. The code gets the first structure value, and puts it in the array. After that, the code adds the structure size to the address memory of pointer
. Now, pointer
points to the next structure value in memory. The process is repeated until all structures, indicated by the len
parameter, are saved in the array.
Using this method, we can now loop through all the devices:
IntPtr pdevs = IntPtr.Zero;
int nDevs = 0;
int input = 0;
int output = 0;
int ring = 0;
if (IAXFunctions.iaxc_audio_devices_get(ref pdevs, ref nDevs,
ref input, ref output, ref ring) != 0)
throw new IAXMethodException("Initialize error!");
object[] devs = MarshalEx.PtrToStructureArray(pdevs,
typeof(iaxc_audio_device), nDevs);
foreach (object o in devs)
{
iaxc_audio_device d = (iaxc_audio_device) o;
...
}
IAX events
The IAXClient
has a message mechanism to fire events using Windows procedures. To receive events, you need to call the iaxc_set_event_callpost
function, passing the window handle that will be used inside IAXClient
to post event messages and the message ID that will be used in the window procedure to indicate that the message comes from IAXClient
:
EXPORT int iaxc_set_event_callpost(void *handle, int id);
In C# we use the IntPtr
type to pass the window handle:
[DllImport(DllImportName)]
public static extern int iaxc_set_event_callpost(
IntPtr handle,
int id);
NativeWindowEx class
To receive the message events, you need to create a message procedure from a custom window. You could do it using the Windows API CreateWindowEx
, DefWindowProc
, CallWindowProc
, etc. Instead, you can use the .NET NativeWindow
class which creates the window, keeps the window handle, and creates a overridable window procedure in which you can receive messages. So, we can inherit the NativeWindow
class and override the WndProc
method:
public class NativeWindowEx : NativeWindow
{
#region Fields
private event OnMessageDelegate FOnMessageEvent;
private GCHandle FPinnedHandle;
#endregion
#region Constructor
public NativeWindowEx(CreateParams createParam, bool pinned)
{
this.CreateHandle(createParam);
if (pinned)
{
FPinnedHandle = GCHandle.Alloc(this.Handle, GCHandleType.Pinned);
}
}
public NativeWindowEx(IntPtr handle, bool pinned)
{
this.AssignHandle(handle);
if (pinned)
{
FPinnedHandle = GCHandle.Alloc(handle, GCHandleType.Pinned);
}
}
#endregion
#region Methods
protected override void WndProc(ref Message m)
{
base.WndProc(ref m);
if (FOnMessageEvent != null)
FOnMessageEvent(ref m);
}
#endregion
#region Properties
public GCHandle PinnedHandle
{
get
{
return FPinnedHandle;
}
}
public event OnMessageDelegate OnMessage
{
add
{
FOnMessageEvent += value;
}
remove
{
FOnMessageEvent -= value;
}
}
#endregion
}
Using the NativeWindowEx
class, we can now pass the window handle to the iaxc_set_event_callpost
:
...
FMessageId = 123456;
FWindow = new NativeWindowEx(new CreateParams(), true);
FWindow.OnMessage += new OnMessageDelegate(FWindow_OnMessage);
if (IAXFunctions.iaxc_set_event_callpost(FWindow.Handle, FMessageId) != 0)
throw new IAXMethodException("Event handler initialize error!");
if (IAXFunctions.iaxc_start_processing_thread() != 0)
throw new IAXMethodException("Event handler start error!");
...
In this code, we first assign the message ID that will be passed to Windows message. After that, we create the NativeWindowEx
class and pass the handle from this class to the iaxc_set_event_callpost
function. When the iaxc_start_processing_thread
function is called, the messages will begin to come in FWindow_OnMessage
.
GCHandle and pinned
Note the GCHandle
class used in NativeWindowEx
. This class is needed because every time you need to pass a managed object to the unmanaged world, you need to pin the class. Remember that when Garbage Collector runs, it tries to reach the objects. If an object is not reached, the object is collected by GC, and, to remove the free space from the collected objects, it moves reached objects to the free spaces, removing heap gaps and invalidating memory address. Using pinned objects, you can prevent the GC to move the object in the heap while the unmanaged application holds the reference to it. Without the pinned object, the object's memory can be moved in managed heap, making the object's address invalid in the unmanaged code. You can read more about GC here.
Event structures
IAXClient
sends information events using the following structure:
typedef struct iaxc_event_struct {
struct iaxc_event_struct *next;
int type;
union {
struct iaxc_ev_levels levels;
struct iaxc_ev_text text;
struct iaxc_ev_call_state call;
struct iaxc_ev_netstats netstats;
struct iaxc_ev_url url;
struct iaxc_ev_video video;
struct iaxc_ev_registration reg;
} ev;
} iaxc_event;
The union
clause is used in the structure indicating that the ev
field value depends on the type
field. So, if you receive a etIAXC_EVENT_LEVELS
value in the type
field, the ev
field points to it. To convert this structure to C#, we use the FieldOffset
attribute (only the main structures converted):
[StructLayout(LayoutKind.Explicit)]
internal struct iaxc_event
{
[FieldOffset(0)]
public IntPtr next;
[FieldOffset(4)]
public EventType type;
[FieldOffset(8)]
public iaxc_ev_levels level;
[FieldOffset(8)]
public iaxc_ev_text text;
[FieldOffset(8)]
public iaxc_ev_call_state callState;
[FieldOffset(8)]
public iaxc_ev_registration registration;
}
FieldOffset
is used with the StructLayout(LayoutKind.Explicit)
clause to indicate the fields's offset in the structure. Using StructLayout(LayoutKind.Explicit)
, all fields must have a FieldOffset
attribute and, to simulate the union
clause, we need to put those fields in the same offset. In this case, we're using [FieldOffset(8)]
in the union fields because the next
and type
fields use four bytes each in the structure's memory.
TypeLoadException
After converting the iaxc_event
structure and running the application, the following TypeLoadException
is thrown:
System.TypeLoadException was unhandled
Message="Could not load type 'ALAZ.TelephonyEx.Voip.IAX.iaxc_event'
from assembly 'ALAZ.TelephonyEx, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null'
because it contains an object field at offset 8 that
is incorrectly aligned or overlapped by a non-object field."
Source="ALAZ.TelephonyEx"
TypeName="ALAZ.TelephonyEx.Voip.IAX.iaxc_event"
StackTrace:
at
...
The exception is thrown because C# doesn't handle value and reference types using the same FieldOffset
attribute. The following iaxc_ev_text
structure is used in iaxc_event
:
...
#define IAXC_EVENT_BUFSIZ 256
...
struct iaxc_ev_text {
int type;
int callNo;
char message[IAXC_EVENT_BUFSIZ];
};
To convert it to C#, we use the MarshalAs
attribute, indicating the correct UnmanagedType
enumeration:
internal struct iaxc_ev_text
{
public TextType type;
public int callNo;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)]
public string message;
}
The UnmanagedType.ByValTStr
enumeration is used to represent the char
array, and the size is defined with the SizeConst
clause. As we are using the string
type in the message
field, C# cannot handle the structure, and throws the exception. If the structures used in the same FieldOffset
attribute have only value types, the exception isn't thrown.
Event message callback
To solve the TypeLoadException
problem, we need to use the .NET Marshal
class and make some changes in the iaxc_event
structure:
internal struct iaxc_event
{
public IntPtr next;
public EventType type;
}
In the new iaxc_event
structure, the union
field is removed and is referenced in NativeWindowEx
's message callback procedure:
private void FWindow_OnMessage(ref System.Windows.Forms.Message m)
{
if (!Disposed)
{
if (m.Msg == FMessageId)
{
iaxc_event e = (iaxc_event)
Marshal.PtrToStructure(m.LParam, typeof(iaxc_event));
IntPtr eventPointer = new IntPtr(m.LParam.ToInt32() + 8);
switch (e.type)
{
case EventType.etIAXC_EVENT_LEVELS:
{
iaxc_ev_levels ev = (iaxc_ev_levels)
Marshal.PtrToStructure(eventPointer,
typeof(iaxc_ev_levels));
...
case EventType.etIAXC_EVENT_REGISTRATION:
{
iaxc_ev_registration ev = (iaxc_ev_registration)
Marshal.PtrToStructure(eventPointer,
typeof(iaxc_ev_registration));
...
case EventType.etIAXC_EVENT_STATE:
{
iaxc_ev_call_state ev = (iaxc_ev_call_state)
Marshal.PtrToStructure(eventPointer,
typeof(iaxc_ev_call_state));
...
case EventType.etIAXC_EVENT_TEXT:
{
iaxc_ev_text ev = (iaxc_ev_text)
Marshal.PtrToStructure(eventPointer,
typeof(iaxc_ev_text));
...
}
eventPointer = IntPtr.Zero;
IAXFunctions.iaxc_free_event(m.LParam);
}
}
}
The FWindow_OnMessage
event receives a System.Windows.Forms.Message
posted by the IAXClient library. After checking if the message is a FMessageId
, we need to cast the message's LParam
property to the iaxc_event
structure using the Marshal.PtrToStructure
method. At this point, e.type
points to the EventType
enumeration, but, where's the event data? The event data is there, but we need to walk through the LParam
address to find it. Remember that the event data field is the third field in the IAXClient
structure. So, we create a new IntPtr
object pointing to 8 bytes (or two 32 bit fields) after the LParam
memory address. The new IntPtr
object now points to the event data field in the memory address, and now it can be cast to the respective event structure using a simple case
statement. After using the event structure, we need to call the iaxc_free_event
function to free the event data.
The IAXClientClass
The IAXClientClass
is the main class to work with the IAXClient library. The IAXClientClass
constructor initializes the the IAXClient library with the AudioType
and the number of lines used, and gets the audio devices available:
public IAXClientClass(AudioType audioType, int lines)
{
FCallerIdName = "SharpIAX";
FCallerIdNumber = "(Not Specified)";
FAudioType = audioType;
FLinesList = new List<IAXLine>();
FLinesCollection = new IAXLines(FLinesList, this);
FAudioDevicesList = new List<IAXAudioDevice>();
FAudioDevicesCollection = new IAXAudioDevices(FAudioDevicesList);
FSilenceThreshold = -99;
FRegistrationRequired = true;
FLines = lines;
if (IAXFunctions.iaxc_initialize((int)FAudioType, FLines) != 0)
throw new IAXMethodException("Initialize error!");
IntPtr pdevs = IntPtr.Zero;
int nDevs = 0;
int input = 0;
int output = 0;
int ring = 0;
if (IAXFunctions.iaxc_audio_devices_get(ref pdevs, ref nDevs,
ref input, ref output, ref ring) != 0)
throw new IAXMethodException("Initialize error!");
FAudioDevicesCollection.SelectedInputAudioDevice = input;
FAudioDevicesCollection.SelectedOutputAudioDevice = output;
FAudioDevicesCollection.SelectedRingAudioDevice = ring;
object[] devs = MarshalEx.PtrToStructureArray(pdevs,
typeof(iaxc_audio_device), nDevs);
foreach (object o in devs)
{
iaxc_audio_device d = (iaxc_audio_device)o;
IAXAudioDevice iaxd = new IAXAudioDevice(d.devID,
d.name, d.capabilities);
FAudioDevicesList.Add(iaxd);
}
}
The Initialize
method sets the caller ID, silence threshold, audio formats, and initializes the message event process. The method adds to the IAXLines
collection the number of IAXLine
items requested and, if registration is required, it begins the registration procedure:
public void Initialize(string user, string password,
string server, bool register)
{
if (!Disposed)
{
FUser = user;
FPass = password;
FServer = server;
FRegistrationRequired = register;
IAXFunctions.iaxc_set_callerid(FCallerIdName, FCallerIdNumber);
IAXFunctions.iaxc_set_silence_threshold(FSilenceThreshold);
FPreferredFormat = AudioFormat.afIAXC_FORMAT_GSM;
FAllowedFormats = FPreferredFormat;
IAXFunctions.iaxc_set_formats((int)FPreferredFormat,
(int)FAllowedFormats);
FMessageId = 123456;
FWindow = new NativeWindowEx(new CreateParams(), true);
FWindow.OnMessage += new OnMessageDelegate(FWindow_OnMessage);
if (IAXFunctions.iaxc_set_event_callpost(FWindow.Handle, FMessageId) != 0)
throw new IAXMethodException("Event handler initialize error!");
if (IAXFunctions.iaxc_start_processing_thread() != 0)
throw new IAXMethodException("Event handler start error!");
for (int i = 1; i <= FLines; i++)
{
FLinesList.Add(new IAXLine(FLinesCollection, i - 1));
}
FLinesCollection.SelectLine(0);
if (FRegistrationRequired)
{
Register();
}
}
}
The Shutdown
method terminates all active calls, unregisters the user, and stops the message event process:
public void ShutDown()
{
if (!Disposed)
{
IAXFunctions.iaxc_dump_all_calls();
if (FRegistrationId > 0)
IAXFunctions.iaxc_unregister(FRegistrationId);
if (IAXFunctions.iaxc_stop_processing_thread() != 0)
throw new IAXMethodException("Event handler stop error!");
IAXFunctions.iaxc_shutdown();
}
}
Playing sounds
IAXClient library has some sounds functions called iaxc_play_sound
and iaxc_stop_sound
. iaxc_play_sound
plays a raw wave buffer and returns a sound ticket. The ticket is used in iaxc_stop_sound
to stop the sound playing. The iaxc_sound
structure must be filled in order to play the sound.
The main fields in iaxc_sound
is data
and len
. data
is the raw wav buffer array to play and len
defines the buffer array length. This structure is used in PlaySound
method:
private void PlaySound(string resourceName, int repeat, ref int Id)
{
Stream fs =
Assembly.GetExecutingAssembly().
GetManifestResourceStream(resourceName);
fs.Position = 44;
byte[] arr = new byte[fs.Length - fs.Position];
fs.Read(arr, 0, arr.Length);
fs.Close();
GCHandle h = GCHandle.Alloc(arr, GCHandleType.Pinned);
iaxc_sound s = new iaxc_sound();
s.len = arr.Length / 2;
s.data = h.AddrOfPinnedObject();
s.malloced = 0;
s.channel = 0;
s.repeat = repeat;
Id = IAXFunctions.iaxc_play_sound(ref s, 0);
h.Free();
}
The resourceName
parameter defines the embedded resource file name. The repeat
parameters defines how many times the song will be played. The Id
parameter defines the ticket returned from iaxc_play_sound
.
Embedded resources
You can embed resource files in Assembly simply setting the resource's Build Action
property to Embedded Resource
:
Wav file header
The data
field from iaxc_sound
structure must be filled with wave raw data and we need to know where the raw data begins inside a wave file. Well, the answer is here. Wave files are divided in chunks and data chunk (raw data) begins at 44th byte in wave files. So, PlaySound
method opens the embedded resource wave file, read it beginning from 44th byte till the end of file. After that, the iaxc_sound
structure is filled with the pinned pointer of the raw data array. Then the sound is played using iaxc_play_sound
function.
IAXAudioDevices
The IAXAudioDevices
is a collection of IAXAudioDevice
s. Each IAXAudioDevice
has ID
, Name
, and Capabilities
properties. Using IAXAudioDevices
methods, you can set the correct device for input, output, and ring:
private void SetAudioDevices()
{
if (IAXFunctions.iaxc_audio_devices_set(FInputDevice,
FOutputDevice, FRingDevice) != 0)
throw new IAXMethodException("Set device error!");
}
public void SelectInputAudioDevice(int deviceId)
{
FInputDevice = deviceId;
SetAudioDevices();
}
public void SelectOutPutAudioDevice(int deviceId)
{
FOutputDevice = deviceId;
SetAudioDevices();
}
public void SelectRingAudioDevice(int deviceId)
{
FRingDevice = deviceId;
SetAudioDevices();
}
IAXLines
The IAXLines
is a collection of IAXLine
s. The SelectLine
method selects the line to work with. To get the selected line, the SelectedLine
property is used.
Each IAXLine
has an ID
, CallTime
, and local and remote call information. The CallDirection
property indicates if the call is an incoming or an outgoing call. The CallStatus
property indicates the status of the call in the line. The MakeCall
method is used to start a call on the selected line, and the DropCall
method is used to terminate the call. The AnswerCall
method answers a ringing line, and TransferCall
transfers the call. To send digits in an active call, you can use the SendDTMF
method.
public void MakeCall(string number)
{
string callMessage = String.Format(CALLFORMAT,
FCollection.Host.User, FCollection.Host.Password,
FCollection.Host.Server, number);
IAXFunctions.iaxc_call(callMessage);
}
public void DropCall()
{
IAXFunctions.iaxc_dump_call();
}
public void AnswerCall()
{
IAXFunctions.iaxc_answer_call(FLineNumber);
}
public void SendDTMF(byte digit)
{
IAXFunctions.iaxc_send_dtmf(digit);
}
public void TransferCall(string number)
{
IAXFunctions.iaxc_blind_transfer_call(FLineNumber, number);
}
SharpIAX demo
The SharpIAX demo is a Windows Forms application that uses the IAXClientClass
class. It uses two lines, and shows the call status of each line, shows IAX messages from the server, and sends DTMF digits:
In the Configuration->Network tab, you can set the server's address, username and password, caller information, and the audio formats. You can also set the audio devices used in input, output, and ring in the Configuration->Audio tab:
Compiling IAXCLient
The SharpIAX demo binaries already uses a compiled version of IAXClient library but, if you wish to compile it, you need to download it from Sourceforge.
CygWin
First of all, you need to download CygWin.
Run the setup application, and choose to install from the internet.
After that, select the root directory (installation directory):
the local package directory (downloaded packages directory):
and the download site.
When the Select Packages form appears, in addition to the default packages, select the gcc-mingw, gcc-mingw-core, gcc-mingw-g++
makefile,
and subversion packages located at the Devel category.
After installation, run the cygwin.bat on the CygWin installation directory to open the Cygwin shell.
Download IAXClient
To download IAXClient sources, you need to execute the svn command. Inside CygWin shell, create a new directory somewhere and, on the new directory, execute the following command (don't forget the dot at the end!):
svn co https://svn.sourceforge.net/svnroot/iaxclient/trunk/ .
Compiling IAXClient
After downloading it, locate the iaxlclient/lib directory and, inside this directory, type the make command to generate the DLL:
make shared
If you get some errors, check the makefile file located in the lib directory. Otherwise, you've a new fresh iaxclient.dll.
Conclusion
I hope that this article helps you to get familiar with the IAXClient library and understand how P/Invoke can be utilized to work with unmanaged applications and structures. I'd like to thank Mr. Adelino Baena from Alcance Brasil for the Asterisk server installation, maintenance, support, and incentive to build this library. Any comments will be appreciated.
History
- 22 Aug, 2006: Initial version.
- 11 Nov, 2006:
- CygWin screenshots added.
- New play methods do play DTMF tones, ring and busy sounds.