Sophie

Sophie

distrib > Mandriva > 2010.1 > x86_64 > media > main-release > by-pkgid > f48b0484566fe5f15f1edab7e7e31247 > files > 30

lib64usb1.0-devel-1.0.7-3mdv2010.1.x86_64.rpm

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
<title>libusb: Multi-threaded applications and asynchronous I/O</title>
<link href="tabs.css" rel="stylesheet" type="text/css"/>
<link href="doxygen.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<!-- Generated by Doxygen 1.6.3 -->
<script type="text/javascript">
<!--
function changeDisplayState (e){
  var num=this.id.replace(/[^[0-9]/g,'');
  var button=this.firstChild;
  var sectionDiv=document.getElementById('dynsection'+num);
  if (sectionDiv.style.display=='none'||sectionDiv.style.display==''){
    sectionDiv.style.display='block';
    button.src='open.gif';
  }else{
    sectionDiv.style.display='none';
    button.src='closed.gif';
  }
}
function initDynSections(){
  var divs=document.getElementsByTagName('div');
  var sectionCounter=1;
  for(var i=0;i<divs.length-1;i++){
    if(divs[i].className=='dynheader'&&divs[i+1].className=='dynsection'){
      var header=divs[i];
      var section=divs[i+1];
      var button=header.firstChild;
      if (button!='IMG'){
        divs[i].insertBefore(document.createTextNode(' '),divs[i].firstChild);
        button=document.createElement('img');
        divs[i].insertBefore(button,divs[i].firstChild);
      }
      header.style.cursor='pointer';
      header.onclick=changeDisplayState;
      header.id='dynheader'+sectionCounter;
      button.src='closed.gif';
      section.id='dynsection'+sectionCounter;
      section.style.display='none';
      section.style.marginLeft='14px';
      sectionCounter++;
    }
  }
}
window.onload = initDynSections;
-->
</script>
<div class="navigation" id="top">
  <div class="tabs">
    <ul>
      <li><a href="index.html"><span>Main&nbsp;Page</span></a></li>
      <li class="current"><a href="pages.html"><span>Related&nbsp;Pages</span></a></li>
      <li><a href="modules.html"><span>Modules</span></a></li>
      <li><a href="annotated.html"><span>Data&nbsp;Structures</span></a></li>
      <li><a href="files.html"><span>Files</span></a></li>
    </ul>
  </div>
</div>
<div class="contents">


<h1><a class="anchor" id="mtasync">Multi-threaded applications and asynchronous I/O </a></h1><p>libusb is a thread-safe library, but extra considerations must be applied to applications which interact with libusb from multiple threads.</p>
<p>The underlying issue that must be addressed is that all libusb I/O revolves around monitoring file descriptors through the poll()/select() system calls. This is directly exposed at the <a class="el" href="group__asyncio.html">asynchronous interface</a> but it is important to note that the <a class="el" href="group__syncio.html">synchronous interface</a> is implemented on top of the asynchonrous interface, therefore the same considerations apply.</p>
<p>The issue is that if two or more threads are concurrently calling poll() or select() on libusb's file descriptors then only one of those threads will be woken up when an event arrives. The others will be completely oblivious that anything has happened.</p>
<p>Consider the following pseudo-code, which submits an asynchronous transfer then waits for its completion. This style is one way you could implement a synchronous interface on top of the asynchronous interface (and libusb does something similar, albeit more advanced due to the complications explained on this page).</p>
<div class="fragment"><pre class="fragment"><span class="keywordtype">void</span> cb(<span class="keyword">struct</span> <a class="code" href="structlibusb__transfer.html" title="The generic USB transfer structure.">libusb_transfer</a> *transfer)
{
    <span class="keywordtype">int</span> *completed = transfer-&gt;<a class="code" href="structlibusb__transfer.html#ab75ab3e7185f08e07a1ae858a35ebb7b" title="User context data to pass to the callback function.">user_data</a>;
    *completed = 1;
}

<span class="keywordtype">void</span> myfunc() {
    <span class="keyword">struct </span><a class="code" href="structlibusb__transfer.html" title="The generic USB transfer structure.">libusb_transfer</a> *transfer;
    <span class="keywordtype">unsigned</span> <span class="keywordtype">char</span> <a class="code" href="structlibusb__transfer.html#a7fa594567e074191ce8f28b5fb4a3bea" title="Data buffer.">buffer</a>[LIBUSB_CONTROL_SETUP_SIZE];
    <span class="keywordtype">int</span> completed = 0;

    transfer = <a class="code" href="group__asyncio.html#ga13cc69ea40c702181c430c950121c000" title="Allocate a libusb transfer with a specified number of isochronous packet descriptors...">libusb_alloc_transfer</a>(0);
    <a class="code" href="group__asyncio.html#ga5447311149ec2bd954b5f1a640a8e231" title="Helper function to populate the setup packet (first 8 bytes of the data buffer) for...">libusb_fill_control_setup</a>(buffer,
        <a class="code" href="group__misc.html#gga0b0933ae70744726cde11254c39fac91a1585f40d2a73c752a5f60688612c1345" title="Vendor.">LIBUSB_REQUEST_TYPE_VENDOR</a> | <a class="code" href="group__desc.html#gga86c880af878493aa8f805c2aba654b8ba940484c16d44bdfc6eccc2de7a9ffcb2" title="Out: host-to-device.">LIBUSB_ENDPOINT_OUT</a>, 0x04, 0x01, 0, 0);
    <a class="code" href="group__asyncio.html#ga3a8513ed87229fe2c9771ef0bf17206e" title="Helper function to populate the required libusb_transfer fields for a control transfer...">libusb_fill_control_transfer</a>(transfer, dev, buffer, cb, &amp;completed, 1000);
    <a class="code" href="group__asyncio.html#gabb0932601f2c7dad2fee4b27962848ce" title="Submit a transfer.">libusb_submit_transfer</a>(transfer);

    <span class="keywordflow">while</span> (!completed) {
        poll(libusb file descriptors, 120*1000);
        <span class="keywordflow">if</span> (poll indicates activity)
            <a class="code" href="group__poll.html#ga6deff4c7d3a6c04bb9ec9fd259b48933" title="Handle any pending events.">libusb_handle_events_timeout</a>(ctx, 0);
    }
    printf(<span class="stringliteral">&quot;completed!&quot;</span>);
    <span class="comment">// other code here</span>
}
</pre></div><p>Here we are <em>serializing</em> completion of an asynchronous event against a condition - the condition being completion of a specific transfer. The poll() loop has a long timeout to minimize CPU usage during situations when nothing is happening (it could reasonably be unlimited).</p>
<p>If this is the only thread that is polling libusb's file descriptors, there is no problem: there is no danger that another thread will swallow up the event that we are interested in. On the other hand, if there is another thread polling the same descriptors, there is a chance that it will receive the event that we were interested in. In this situation, <code>myfunc()</code> will only realise that the transfer has completed on the next iteration of the loop, <em>up to 120 seconds later.</em> Clearly a two-minute delay is undesirable, and don't even think about using short timeouts to circumvent this issue!</p>
<p>The solution here is to ensure that no two threads are ever polling the file descriptors at the same time. A naive implementation of this would impact the capabilities of the library, so libusb offers the scheme documented below to ensure no loss of functionality.</p>
<p>Before we go any further, it is worth mentioning that all libusb-wrapped event handling procedures fully adhere to the scheme documented below. This includes <a class="el" href="group__poll.html#ga4989086e3f0327f3886a4c474ec7c327" title="Handle any pending events in blocking mode.">libusb_handle_events()</a> and all the synchronous I/O functions - libusb hides this headache from you. You do not need to worry about any of these issues if you stick to that level.</p>
<p>The problem is when we consider the fact that libusb exposes file descriptors to allow for you to integrate asynchronous USB I/O into existing main loops, effectively allowing you to do some work behind libusb's back. If you do take libusb's file descriptors and pass them to poll()/select() yourself, you need to be aware of the associated issues.</p>
<h2><a class="anchor" id="eventlock">
The events lock</a></h2>
<p>The first concept to be introduced is the events lock. The events lock is used to serialize threads that want to handle events, such that only one thread is handling events at any one time.</p>
<p>You must take the events lock before polling libusb file descriptors, using <a class="el" href="group__poll.html#gaa72153938dc4f34decfacbc6cc6237ef" title="Acquire the event handling lock, blocking until successful acquisition if it is contended...">libusb_lock_events()</a>. You must release the lock as soon as you have aborted your poll()/select() loop, using <a class="el" href="group__poll.html#gacefbeabdd3409490dc4678f00779c165" title="Release the lock previously acquired with libusb_try_lock_events() or libusb_lock_events()...">libusb_unlock_events()</a>.</p>
<h2><a class="anchor" id="threadwait">
Letting other threads do the work for you</a></h2>
<p>Although the events lock is a critical part of the solution, it is not enough on it's own. You might wonder if the following is sufficient... </p>
<div class="fragment"><pre class="fragment">    <a class="code" href="group__poll.html#gaa72153938dc4f34decfacbc6cc6237ef" title="Acquire the event handling lock, blocking until successful acquisition if it is contended...">libusb_lock_events</a>(ctx);
    <span class="keywordflow">while</span> (!completed) {
        poll(libusb file descriptors, 120*1000);
        <span class="keywordflow">if</span> (poll indicates activity)
            <a class="code" href="group__poll.html#ga6deff4c7d3a6c04bb9ec9fd259b48933" title="Handle any pending events.">libusb_handle_events_timeout</a>(ctx, 0);
    }
    <a class="code" href="group__poll.html#gacefbeabdd3409490dc4678f00779c165" title="Release the lock previously acquired with libusb_try_lock_events() or libusb_lock_events()...">libusb_unlock_events</a>(ctx);
</pre></div><p> ...and the answer is that it is not. This is because the transfer in the code shown above may take a long time (say 30 seconds) to complete, and the lock is not released until the transfer is completed.</p>
<p>Another thread with similar code that wants to do event handling may be working with a transfer that completes after a few milliseconds. Despite having such a quick completion time, the other thread cannot check that status of its transfer until the code above has finished (30 seconds later) due to contention on the lock.</p>
<p>To solve this, libusb offers you a mechanism to determine when another thread is handling events. It also offers a mechanism to block your thread until the event handling thread has completed an event (and this mechanism does not involve polling of file descriptors).</p>
<p>After determining that another thread is currently handling events, you obtain the <em>event waiters</em> lock using <a class="el" href="group__poll.html#ga150865a3f35c38173d688efa7ee52929" title="Acquire the event waiters lock.">libusb_lock_event_waiters()</a>. You then re-check that some other thread is still handling events, and if so, you call <a class="el" href="group__poll.html#gae22755d523560be2867be7d09034ca50" title="Wait for another thread to signal completion of an event.">libusb_wait_for_event()</a>.</p>
<p><a class="el" href="group__poll.html#gae22755d523560be2867be7d09034ca50" title="Wait for another thread to signal completion of an event.">libusb_wait_for_event()</a> puts your application to sleep until an event occurs, or until a thread releases the events lock. When either of these things happen, your thread is woken up, and should re-check the condition it was waiting on. It should also re-check that another thread is handling events, and if not, it should start handling events itself.</p>
<p>This looks like the following, as pseudo-code: </p>
<div class="fragment"><pre class="fragment">retry:
<span class="keywordflow">if</span> (<a class="code" href="group__poll.html#ga6e5a116d5c9498ca4a0e29587fec1a05" title="Attempt to acquire the event handling lock.">libusb_try_lock_events</a>(ctx) == 0) {
    <span class="comment">// we obtained the event lock: do our own event handling</span>
    <span class="keywordflow">while</span> (!completed) {
        <span class="keywordflow">if</span> (!<a class="code" href="group__poll.html#ga63592b28c265185d9469d1e6920d8373" title="Determine if it is still OK for this thread to be doing event handling.">libusb_event_handling_ok</a>(ctx)) {
            <a class="code" href="group__poll.html#gacefbeabdd3409490dc4678f00779c165" title="Release the lock previously acquired with libusb_try_lock_events() or libusb_lock_events()...">libusb_unlock_events</a>(ctx);
            <span class="keywordflow">goto</span> retry;
        }
        poll(libusb file descriptors, 120*1000);
        <span class="keywordflow">if</span> (poll indicates activity)
            <a class="code" href="group__poll.html#ga71da081f97afa3bf68aed8e372254e8f" title="Handle any pending events by polling file descriptors, without checking if any other...">libusb_handle_events_locked</a>(ctx, 0);
    }
    <a class="code" href="group__poll.html#gacefbeabdd3409490dc4678f00779c165" title="Release the lock previously acquired with libusb_try_lock_events() or libusb_lock_events()...">libusb_unlock_events</a>(ctx);
} <span class="keywordflow">else</span> {
    <span class="comment">// another thread is doing event handling. wait for it to signal us that</span>
    <span class="comment">// an event has completed</span>
    <a class="code" href="group__poll.html#ga150865a3f35c38173d688efa7ee52929" title="Acquire the event waiters lock.">libusb_lock_event_waiters</a>(ctx);

    <span class="keywordflow">while</span> (!completed) {
        <span class="comment">// now that we have the event waiters lock, double check that another</span>
        <span class="comment">// thread is still handling events for us. (it may have ceased handling</span>
        <span class="comment">// events in the time it took us to reach this point)</span>
        <span class="keywordflow">if</span> (!<a class="code" href="group__poll.html#ga3a0a6e8be310c20f1ca68722149f9dbf" title="Determine if an active thread is handling events (i.e.">libusb_event_handler_active</a>(ctx)) {
            <span class="comment">// whoever was handling events is no longer doing so, try again</span>
            <a class="code" href="group__poll.html#ga41d7716458c11ee02d0deb19a31233ed" title="Release the event waiters lock.">libusb_unlock_event_waiters</a>(ctx);
            <span class="keywordflow">goto</span> retry;
        }
    
        <a class="code" href="group__poll.html#gae22755d523560be2867be7d09034ca50" title="Wait for another thread to signal completion of an event.">libusb_wait_for_event</a>(ctx);
    }
    <a class="code" href="group__poll.html#ga41d7716458c11ee02d0deb19a31233ed" title="Release the event waiters lock.">libusb_unlock_event_waiters</a>(ctx);
}
printf(<span class="stringliteral">&quot;completed!\n&quot;</span>);
</pre></div><p>A naive look at the above code may suggest that this can only support one event waiter (hence a total of 2 competing threads, the other doing event handling), because the event waiter seems to have taken the event waiters lock while waiting for an event. However, the system does support multiple event waiters, because <a class="el" href="group__poll.html#gae22755d523560be2867be7d09034ca50" title="Wait for another thread to signal completion of an event.">libusb_wait_for_event()</a> actually drops the lock while waiting, and reaquires it before continuing.</p>
<p>We have now implemented code which can dynamically handle situations where nobody is handling events (so we should do it ourselves), and it can also handle situations where another thread is doing event handling (so we can piggyback onto them). It is also equipped to handle a combination of the two, for example, another thread is doing event handling, but for whatever reason it stops doing so before our condition is met, so we take over the event handling.</p>
<p>Four functions were introduced in the above pseudo-code. Their importance should be apparent from the code shown above.</p>
<ol type="1">
<li><a class="el" href="group__poll.html#ga6e5a116d5c9498ca4a0e29587fec1a05" title="Attempt to acquire the event handling lock.">libusb_try_lock_events()</a> is a non-blocking function which attempts to acquire the events lock but returns a failure code if it is contended.</li>
<li><a class="el" href="group__poll.html#ga63592b28c265185d9469d1e6920d8373" title="Determine if it is still OK for this thread to be doing event handling.">libusb_event_handling_ok()</a> checks that libusb is still happy for your thread to be performing event handling. Sometimes, libusb needs to interrupt the event handler, and this is how you can check if you have been interrupted. If this function returns 0, the correct behaviour is for you to give up the event handling lock, and then to repeat the cycle. The following <a class="el" href="group__poll.html#ga6e5a116d5c9498ca4a0e29587fec1a05" title="Attempt to acquire the event handling lock.">libusb_try_lock_events()</a> will fail, so you will become an events waiter. For more information on this, read <a class="el" href="mtasync.html#fullstory">The full story</a> below.</li>
<li><a class="el" href="group__poll.html#ga71da081f97afa3bf68aed8e372254e8f" title="Handle any pending events by polling file descriptors, without checking if any other...">libusb_handle_events_locked()</a> is a variant of <a class="el" href="group__poll.html#ga6deff4c7d3a6c04bb9ec9fd259b48933" title="Handle any pending events.">libusb_handle_events_timeout()</a> that you can call while holding the events lock. <a class="el" href="group__poll.html#ga6deff4c7d3a6c04bb9ec9fd259b48933" title="Handle any pending events.">libusb_handle_events_timeout()</a> itself implements similar logic to the above, so be sure not to call it when you are "working behind libusb's back", as is the case here.</li>
<li><a class="el" href="group__poll.html#ga3a0a6e8be310c20f1ca68722149f9dbf" title="Determine if an active thread is handling events (i.e.">libusb_event_handler_active()</a> determines if someone is currently holding the events lock</li>
</ol>
<p>You might be wondering why there is no function to wake up all threads blocked on <a class="el" href="group__poll.html#gae22755d523560be2867be7d09034ca50" title="Wait for another thread to signal completion of an event.">libusb_wait_for_event()</a>. This is because libusb can do this internally: it will wake up all such threads when someone calls <a class="el" href="group__poll.html#gacefbeabdd3409490dc4678f00779c165" title="Release the lock previously acquired with libusb_try_lock_events() or libusb_lock_events()...">libusb_unlock_events()</a> or when a transfer completes (at the point after its callback has returned).</p>
<h3><a class="anchor" id="fullstory">
The full story</a></h3>
<p>The above explanation should be enough to get you going, but if you're really thinking through the issues then you may be left with some more questions regarding libusb's internals. If you're curious, read on, and if not, skip to the next section to avoid confusing yourself!</p>
<p>The immediate question that may spring to mind is: what if one thread modifies the set of file descriptors that need to be polled while another thread is doing event handling?</p>
<p>There are 2 situations in which this may happen.</p>
<ol type="1">
<li><a class="el" href="group__dev.html#ga8163100afdf933fabed0db7fa81c89d1" title="Open a device and obtain a device handle.">libusb_open()</a> will add another file descriptor to the poll set, therefore it is desirable to interrupt the event handler so that it restarts, picking up the new descriptor.</li>
<li><a class="el" href="group__dev.html#ga779bc4f1316bdb0ac383bddbd538620e" title="Close a device handle.">libusb_close()</a> will remove a file descriptor from the poll set. There are all kinds of race conditions that could arise here, so it is important that nobody is doing event handling at this time.</li>
</ol>
<p>libusb handles these issues internally, so application developers do not have to stop their event handlers while opening/closing devices. Here's how it works, focusing on the <a class="el" href="group__dev.html#ga779bc4f1316bdb0ac383bddbd538620e" title="Close a device handle.">libusb_close()</a> situation first:</p>
<ol type="1">
<li>During initialization, libusb opens an internal pipe, and it adds the read end of this pipe to the set of file descriptors to be polled.</li>
<li>During <a class="el" href="group__dev.html#ga779bc4f1316bdb0ac383bddbd538620e" title="Close a device handle.">libusb_close()</a>, libusb writes some dummy data on this control pipe. This immediately interrupts the event handler. libusb also records internally that it is trying to interrupt event handlers for this high-priority event.</li>
<li>At this point, some of the functions described above start behaving differently:<ul>
<li><a class="el" href="group__poll.html#ga63592b28c265185d9469d1e6920d8373" title="Determine if it is still OK for this thread to be doing event handling.">libusb_event_handling_ok()</a> starts returning 1, indicating that it is NOT OK for event handling to continue.</li>
<li><a class="el" href="group__poll.html#ga6e5a116d5c9498ca4a0e29587fec1a05" title="Attempt to acquire the event handling lock.">libusb_try_lock_events()</a> starts returning 1, indicating that another thread holds the event handling lock, even if the lock is uncontended.</li>
<li><a class="el" href="group__poll.html#ga3a0a6e8be310c20f1ca68722149f9dbf" title="Determine if an active thread is handling events (i.e.">libusb_event_handler_active()</a> starts returning 1, indicating that another thread is doing event handling, even if that is not true.</li>
</ul>
</li>
<li>The above changes in behaviour result in the event handler stopping and giving up the events lock very quickly, giving the high-priority <a class="el" href="group__dev.html#ga779bc4f1316bdb0ac383bddbd538620e" title="Close a device handle.">libusb_close()</a> operation a "free ride" to acquire the events lock. All threads that are competing to do event handling become event waiters.</li>
<li>With the events lock held inside <a class="el" href="group__dev.html#ga779bc4f1316bdb0ac383bddbd538620e" title="Close a device handle.">libusb_close()</a>, libusb can safely remove a file descriptor from the poll set, in the safety of knowledge that nobody is polling those descriptors or trying to access the poll set.</li>
<li>After obtaining the events lock, the close operation completes very quickly (usually a matter of milliseconds) and then immediately releases the events lock.</li>
<li>At the same time, the behaviour of <a class="el" href="group__poll.html#ga63592b28c265185d9469d1e6920d8373" title="Determine if it is still OK for this thread to be doing event handling.">libusb_event_handling_ok()</a> and friends reverts to the original, documented behaviour.</li>
<li>The release of the events lock causes the threads that are waiting for events to be woken up and to start competing to become event handlers again. One of them will succeed; it will then re-obtain the list of poll descriptors, and USB I/O will then continue as normal.</li>
</ol>
<p><a class="el" href="group__dev.html#ga8163100afdf933fabed0db7fa81c89d1" title="Open a device and obtain a device handle.">libusb_open()</a> is similar, and is actually a more simplistic case. Upon a call to <a class="el" href="group__dev.html#ga8163100afdf933fabed0db7fa81c89d1" title="Open a device and obtain a device handle.">libusb_open()</a>:</p>
<ol type="1">
<li>The device is opened and a file descriptor is added to the poll set.</li>
<li>libusb sends some dummy data on the control pipe, and records that it is trying to modify the poll descriptor set.</li>
<li>The event handler is interrupted, and the same behaviour change as for <a class="el" href="group__dev.html#ga779bc4f1316bdb0ac383bddbd538620e" title="Close a device handle.">libusb_close()</a> takes effect, causing all event handling threads to become event waiters.</li>
<li>The <a class="el" href="group__dev.html#ga8163100afdf933fabed0db7fa81c89d1" title="Open a device and obtain a device handle.">libusb_open()</a> implementation takes its free ride to the events lock.</li>
<li>Happy that it has successfully paused the events handler, <a class="el" href="group__dev.html#ga8163100afdf933fabed0db7fa81c89d1" title="Open a device and obtain a device handle.">libusb_open()</a> releases the events lock.</li>
<li>The event waiter threads are all woken up and compete to become event handlers again. The one that succeeds will obtain the list of poll descriptors again, which will include the addition of the new device.</li>
</ol>
<h3><a class="anchor" id="concl">
Closing remarks</a></h3>
<p>The above may seem a little complicated, but hopefully I have made it clear why such complications are necessary. Also, do not forget that this only applies to applications that take libusb's file descriptors and integrate them into their own polling loops.</p>
<p>You may decide that it is OK for your multi-threaded application to ignore some of the rules and locks detailed above, because you don't think that two threads can ever be polling the descriptors at the same time. If that is the case, then that's good news for you because you don't have to worry. But be careful here; remember that the synchronous I/O functions do event handling internally. If you have one thread doing event handling in a loop (without implementing the rules and locking semantics documented above) and another trying to send a synchronous USB transfer, you will end up with two threads monitoring the same descriptors, and the above-described undesirable behaviour occuring. The solution is for your polling thread to play by the rules; the synchronous I/O functions do so, and this will result in them getting along in perfect harmony.</p>
<p>If you do have a dedicated thread doing event handling, it is perfectly legal for it to take the event handling lock for long periods of time. Any synchronous I/O functions you call from other threads will transparently fall back to the "event waiters" mechanism detailed above. The only consideration that your event handling thread must apply is the one related to <a class="el" href="group__poll.html#ga63592b28c265185d9469d1e6920d8373" title="Determine if it is still OK for this thread to be doing event handling.">libusb_event_handling_ok()</a>: you must call this before every poll(), and give up the events lock if instructed. </p>
</div>
<hr class="footer"/><address style="text-align: right;"><small>Generated on Wed Apr 28 09:03:58 2010 for libusb by&nbsp;
<a href="http://www.doxygen.org/index.html">
<img class="footer" src="doxygen.png" alt="doxygen"/></a> 1.6.3 </small></address>
</body>
</html>