Thursday, 18 June 2015

Free Accounting Software

I have continued to work on my accounting software, while using it in my business and for my personal accounts (in separate databases).

I have added a number of new functions:

  • Import statements from QIF files
  • Copy and paste statement data from bank web pages
  • Investments (shares, unit trusts, etc.)
  • Import data from Quicken
I'm really keen to get some more beta testers - anyone running Windows or Linux want to try a free small business accounting or personal package to run on their own computer, but which they can access from any computer, phone or tablet?


Thursday, 9 April 2015

Quick Books replacement

I knew it was a while since I had posted anything on here, but I didn't realize how long! Comes of being too busy, I guess.

In my small business, I have been using Quick Books for years. Although when I started the business I had been working for an accounting software company, I still chose Quick books because it was simple and easy to use - no remembering obscure account codes or customer numbers, everything works by name.

Since upgrading to Windows 8, Quick Books has become significantly less functional - none of the pages that use the Internet Explorer web control work any more - they just come out blank. However, it is no longer possible to upgrade or buy Quick books - you have to subscribe to a web service instead.

Doing this was just not worthwhile for a small business like mine, so I had a look at alternatives. Unfortunately, none of the alternatives I could find would import my existing Quick Books data. As I know a lot about accounting software, and figured there would be lots of people in the same position as me, I decided I should write my own.

I have just completed a beta test version, which has the following features:

  • Imports data exported from Quick Books. Journals are exported using a Quick Books report. The only limitation is that all sub-accounts must have unique names, as Quick Books does not distinguish between sub-accounts and main accounts in its journal report.
  • Does all the basic accounting tasks:
    • Sales invoices, credit notes, payments
    • Customer maintenance
    • Product maintenance
    • Optional Days:Hours:Minutes or Hours:Minutes units on product quantities, to make life easier for people like me who do hourly billing.
    • Purchase invoices, credit notes and payments
    • Supplier maintenance
    • VAT
    • Banking, cheques, deposits, transfers, reconcile
    • Name maintenance
    • Accounting, journals, VAT return
    • Reminders
    • Memorised transactions
    • Import (from Quick Books, CSV or tab delimited files)
    • Data integrity check
    • Backup and restore data to json format
    • Audit trail shows details of every transaction added/deleted or changed
    • Payment history shows details of how each payment is allocated to invoices.
  • Provides helpful features like Quick Books:
    • Names (Customer, Supplier, Cheque payee, etc.) all either selected from drop-down lists, or use auto-complete (type part of a name, and choose the full name from a drop-down list).
    • Choosing an existing name for a payment, cheque, journal, etc. automatically populates the form with whatever values were used last time.
  • Uses either SQLite or MySQL database (set in a configuration file)
  • Runs as a Web Server (on port set in configuration file), so can be accessed by any web browser (Chrome recommended for best usability). Tablet and phone friendly, so you can use it from anywhere with access to the computer running the server.
  • Runs on Windows or Linux (uses Mono on Linux, .Net on Windows).
  • Customisable - the user interface is all in HTML/CSS/Javascript, so if you understand these you can customise the user interface however you like.


    I am therefore looking for people who would like to beta test the software. You get a free copy of the software, along with updates as bugs are identified and fixed. You also get free support via email during the beta test programme. Add a comment here, or contact me via my web site http://www.trumphurst.com.

    Tuesday, 19 March 2013

    0x8024402c the proxy server or target server names can't be resolved with Microsoft Security Essentials

    I found the above error on a Windows XP machine, when trying to update the virus definitions.

    I checked the Internet Explorer proxy settings - automatically determine was not checked, and there was no proxy setting.

    I also ran proxycfg -d, just to make sure.

    This still didn't fix it.

    In the end, I ran wireshark during the update attempt. I discovered that, despite the proxy settings being turned off, MSE was still querying the dns for host name wpad, and downloading the proxy setting script from http://wpad/wpad.dat.

    Which would have been fine, except that I had a stray CNAME record for wpad in my local DNS, that pointed to my local webserver, that had a stray wpad.dat file in it, which set the proxy to a machine that no longer exists!

    What was worse, the stray entry only existed in one of the two DNS servers on my network, so the problem was intermittent, depending on which DNS server Windows used for the query.

    Once I deleted the stray DNS entry, and removed the wpad.dat file from my web server, it all started working.

    The moral of the story is, if you experience the same problem, try typing http://wpad/wpad.dat into your web browser, and see if you get a file. If you do, open it with a text editor, and it will show you what proxy settings MSE will decide to use (even if you have told it not to)!

    Thursday, 3 January 2013

    Windows Vista Media Centre Program Guide disappeared on 1 Jan 2013

    I have had a Windows Media Centre almost since they were first invented. When I bought it, it ran Windows XP, but I "upgraded" it to Vista when Vista came out. (Although Vista had many problems, the Media Centre part was better than the XP version.)

    On 1st Jan 2013, the program guide suddenly stopped working. It claimed it had downloaded the guide, but every program had "No data available" on it.

    After some research on the 'Net, I discovered that the UK TV guide was downloaded from Broadcasting Data Services, and that Microsoft's agreement with them appeared to expire on 31 December 2012!

    I spent a whole day investigating what to do about this. Microsoft were no help at all. I considered upgrading to Windows 8, but the Microsoft free offer of a Media Centre registration key via email doesn't work (at least, I had no reply to the request I sent a week ago).

    The only obvious solution I found on the net was to use a separate program to extract the guide information from the broadcast EPG, and put it into Media Centre. To that end, I installed a program called EPG Collector from SourceForge, installed it, and got it working. Except that it couldn't put the data into Media Centre, with the error message "Could not load file or assembly 'mcepg, Version=6.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'".

    Further research, and a posting to the relevant forum, suggested I install something called the Microsoft TV Pack, an upgrade to Media Centre - this does not seem to be available from Microsoft, but I found it at http://digiex.net/guides-reviews/guides-tutorials/media-guides/699-windows-media-center-tv-pack-2008-download-installation-guide.html. I downloaded it, and the PlayReady PC Runtime (also from Microsoft, and linked from the same page), and installed them both.

    I restarted the PC, and Media Centre - all my TV settings had gone! (This is normal, I think, as TV Pack is a major upgrade.) I went into the option to set up TV, and it appeared to hang at the point where it should have downloaded the guide. I panicked, and did some more research - others had the same problem, but none of their solutions seemed to work. So I started it again, and left it overnight.

    In the morning, success! It had probably abandoned the download, and continued - it had scanned the available TV channels, and I was able to proceed (after another delay while it tried to download guide information again).

    I then laboriously went through all the channels (some of which had got guide information from somewhere, and some hadn't). I pressed the "i" button while the cursor was on a channel name in the guide, and chose "Edit Channel" (this is a new option, which only appears after you have installed TV Pack). This gave me the option to renumber the channel (for some reason Al Jazeera had joined BBC2 on channel 2!), and to set up listings for the channel. In most cases, there was an option to "Use listings from broadcaster" - which I assume gets the listings from the broadcast signal, just as a Freeview TV would. There were a few obscure channels where this was not an option, but I left those alone (I wasn't interested in any of them, and most had too weak a signal to actually get a picture).

    I now have a full guide, and I am hoping I will not need to use EPG Collector after all. I have lost all my series recording settings, but they were about due for a clear out anyway.

    Friday, 28 December 2012

    Making a cheap concertina blind using greenhouse shading


    Vertical blind, half open
    Roof blinds - 1 closed, 1 open

    For Christmas, I wanted to give my wife a set of greenhouse shading blinds for her new greenhouse.

    However, when I priced them up, the cost was going to be over £500. I decided to make some instead.

    Turns out they are not difficult to do, and, as I could find no instructions on the Web, I decided to write up how to do it here on this blog.

    The blinds shown here can be used vertically (as in the picture above), or horizontally (on the roof). In either case the moving rail (white in the picture) always stays parallel to the top and bottom rails, due to the guide lines threaded through it.

    The blinds can easily be opened and closed by pulling or pushing either end of the moving rail, and will stay in any position.

    Materials

    • Greenhouse shade netting
    • Strimmer line
    • Plastic electrical conduit, oval section
    • Wood strips (about 1" x 1" - 25cm x 25cm)
    • Rust proof screws (brass or stainless steel)

    Method

    First measure the area to be covered. I suggest making the blinds no wider than about 4 ft (120 cm), as the electrical conduit which forms the handle to open and close the blind bends if you use longer lengths.
    Cut the shade netting to the width to be covered, and about one and a quarter times the length.

    Pleating the blind


    Using a ruler to guide the pleating

    Using an iron on the appropriate setting for your material (nylon for shade netting - not too hot, or it will melt), fold over about an inch of one end of the blind, and iron the fold in.

    I used a long metal ruler about an inch wide to help me get the folds straight and the correct width.

    Fold and iron again, and repeat until you have pleated the full length of the blind.

    I found the weight of the material hanging off the ironing board tended to pull the pleats out, so I used some weights (a couple of square bottles of water in the picture) to stop this happening.

    Obviously you will have to unfold earlier pleats as you go, otherwise the material gets too thick and difficult to handle.

    The pleats do not have to be totally accurate, but it is important to make sure they are reasonable square to the edges of the material (and that the material is cut squarely).

    When you have finished, gather the pleats up carefully (I found starting from the middle was easiest), and tie with some pieces of string to hold them in place. Make sure the edges are all squared up.

    Make the mechanism

    Pleated blind with top rail threaded


    Cut 2 pieces of the wood the same width as the blind, and 1 piece of the electrical conduit.

    Drill holes for the strimmer cord - one close to each end, and one in the middle. The holes in the 2 pieces of wood and the conduit should all line up. Also drill diagonal holes in the ends of each piece of wood as shown.

    Thread the blind

    Threading diagram

    The 3 main strimmer lines should be a little longer than the drop of the blind (enough to allow plenty for knots, and a bit extra for adjustment). The 2 guide lines (the ones that go through the conduit) should be longer by the length of the conduit - best to allow a bit more for adjustment, as these lines should not be as tight as the main lines.

    First thread the two guide lines though the length of the conduit. Then thread the 3 main strimmer lines through the top wooden rail, the blind, the conduit and the bottom rail as shown. Tie figure eight knots in the ends.

    Thread the ends of the guide lines though the diagonal holes - notice that one line goes from top left, through the conduit, and down to bottom right, and the other vice versa. Again, tie them off with figure eight knots.
    Now fasten the blind to the top rail (I used a staple gun), and to the conduit (I used gaffer/duck/duct tape).
    Detail showing guide line (blue) threaded in top rail and conduit

    Fit

    Drill holes in the top and bottom rails for the fitting screws - the placing depends on what you are screwing them to. Fasten the top rail in place, and hold the bottom rail where it is to go. Adjust the length of the main lines so they will be quite tight (a little stretched) when the bottom rail is in place. Temporarily fasten the bottom rail, ready to adjust the guide lines. For horizontal blinds the guide lines can be quite loose, but for vertical blinds, they must be tight enough so the blind does not close under its own weight. Once you have measured and tested, undo the bottom rail and tie the knots to make the lines the right length. Professionally made blinds do not use knots, they have a small piece of metal tube with a grub screw, so you can pull the right length of line through the tube, and tighten the screw.



    Wednesday, 3 October 2012

    Using reCAPTCHA from C++

    I wanted to add reCAPTCHA to some old C++ CGI code, but I couldn't find a reCAPTCHA library for C++. Neither could a few other people I saw looking. So I wrote the following code, which I am happy to share:
    #include <string>
    #include <iostream>
    #include <sstream>
    #include <fstream>
    #include <iomanip>
    #include <curl/curl.h>
    
    static ostringstream curl_result;
    
    /* curl write callback, to fill tidy's input buffer...  */
    uint write_cb(char *in, uint size, uint nmemb, void *out)
    {
      int r = size * nmemb;
      in[r] = '\0';
      curl_result << in;
      return(r);
    }
    
    static bool recaptcha_check_answer(const char *private_key, const char *remoteip, 
     const char *challenge, const char *response, std::string &error) {
     if (!remoteip || !*remoteip) {
      error = "For security reasons, you must pass the remote ip to reCAPTCHA";
      return false;
     }
     //discard spam submissions
     if (!challenge || !*challenge || !response || !*response) {
      error = "Captcha challenge or response missing";
      return false;
     }
    
     CURL *curl;
     CURLcode res;
     struct curl_httppost *formpost=NULL;
     struct curl_httppost *lastptr=NULL;
    
     if(curl_global_init(CURL_GLOBAL_SSL)) {
      error = "Error initialising curl library";
      return false;
     }
     curl = curl_easy_init();
     if(!curl) {
      error = "Error initialising curl library";
      return false;
     }
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "privatekey",
      CURLFORM_COPYCONTENTS, private_key,
      CURLFORM_END);
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "remoteip",
      CURLFORM_COPYCONTENTS, remoteip,
      CURLFORM_END);
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "challenge",
      CURLFORM_COPYCONTENTS, challenge,
      CURLFORM_END);
     curl_formadd(&formpost,
      &lastptr,
      CURLFORM_COPYNAME, "response",
      CURLFORM_COPYCONTENTS, response,
      CURLFORM_END);
    
     const char *surl = "https://api-verify.recaptcha.net/verify";
     curl_easy_setopt(curl, CURLOPT_URL, surl);
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 1L);
     curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost);
     #ifdef SKIP_HOSTNAME_VERFICATION
     /*
      * If the site you're connecting to uses a different host name that what
      * they have mentioned in their server certificate's commonName (or
      * subjectAltName) fields, libcurl will refuse to connect. You can skip
      * this check, but this will make the connection less secure.
      */
     curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
     #endif
     curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    
     res = curl_easy_perform(curl);
    
     /* always cleanup */
     curl_easy_cleanup(curl);
     if(res && res != CURLE_RECV_ERROR)
      {
      std::ostringstream err;
      err << "Error " << res << " contacting captcha server";
      error = err.str();
      return false;
      }
    
     std::string resp = curl_result.str();
     std::string::size_type pos = resp.find("\n");
     std::string part1 = pos == std::string::npos ? resp : resp.substr(0, pos);
     if(part1 == "true")
      return true;
     error = pos == std::string::npos ? "Unknown captcha error" : "Captcha error:" + resp.substr(pos);
     return false;
    }
    
    This code relies on the curl library to handle the communication with the reCAPTCHA server.

    Thursday, 6 September 2012

    Using the word breaker from Microsoft SQL Server 2005 in a stand-alone C# program


    I have been writing a program which will query a full text search index on Microsoft SQL Server 2005. The data being indexed are large blobs of text (multiple lines).
    I need to extract only those lines of text which contain the search words for display with the results.
    Full Text Search uses a word breaker (IWordBreaker COM interface) and stemmer (IStemmer COM interface) to break the indexed text into words, and to enable matching of alternate word forms (e.g. "welcome" will also match "welcomes", "welcomed" and "welcoming").
    I therefore need to use the Sql Server word breaker and stemmer to determine if each line of text from the blob matches the search string, as I believe the standard word breaker and stemmer in Windows search uses a different algorithm.
    I thought if I copied the two dlls (infosoft and langwrbk) from a SQL Server installation to the machine on which I am running my program, and ran regsvr32 on them, they would be installed in the registry, and I would be able to use them. Unfortunately, this does not work, as regsvr32 does not add any information to the registry.
    I therefore needed to load the COM components in the dlls "by hand". I started with John Jeffery's code to load COM components by hand. Once I had got over some 32-bit/64-bit problems (your DLL has to match the bit size of your calling code), it worked.
    Here is the LinqPad script I used to test the two interfaces, in case someone else needs to do the same thing:

    const string SqlServerDllFolder = @"C:\Program Files\Microsoft SQL Server\MSSQL.1\MSSQL\Binn";
    static LibraryModule langwrbk = LibraryModule.LoadModule(Path.Combine(SqlServerDllFolder, "LangWrbk.dll"));
    static LibraryModule infosoft = LibraryModule.LoadModule(Path.Combine(SqlServerDllFolder, "infosoft.dll"));
    static Guid stemmer = new Guid("D99F7670-7F1A-11CE-BE57-00AA0051FE20");
    static Guid breaker = new Guid("173C97E2-AEBE-437C-9445-01B237ABF2F6");
    
    void Main()
    {
     WordBreaker breaker = new WordBreaker();
     string search;
     while((search = Util.ReadLine("Query string:").Trim()) != "") {
      Console.WriteLine("Original text:" + search);
      foreach (string word in breaker.Search(search)) {
       Console.WriteLine(word);
      }
     }
    }
    
    // COM Interface to Microsoft word breaker and stemmer
    [Flags]
    public enum WORDREP_BREAK_TYPE {
     WORDREP_BREAK_EOW = 0,
     WORDREP_BREAK_EOS = 1,
     WORDREP_BREAK_EOP = 2,
     WORDREP_BREAK_EOC = 3
    }
    
    [ComImport]
    [Guid("CC907054-C058-101A-B554-08002B33B0E6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IWordSink {
     void PutWord([MarshalAs(UnmanagedType.U4)] int cwc,
      [MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcLen,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcPos);
     void PutAltWord([MarshalAs(UnmanagedType.U4)] int cwc,
      [MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcLen,
      [MarshalAs(UnmanagedType.U4)] int cwcSrcPos);
     void StartAltPhrase();
     void EndAltPhrase();
     void PutBreak(WORDREP_BREAK_TYPE breakType);
    }
    
    [ComImport]
    [Guid("CC906FF0-C058-101A-B554-08002B33B0E6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IPhraseSink {
     void PutSmallPhrase([MarshalAs(UnmanagedType.LPWStr)] string pwcNoun,
      [MarshalAs(UnmanagedType.U4)] int cwcNoun,
      [MarshalAs(UnmanagedType.LPWStr)] string pwcModifier,
      [MarshalAs(UnmanagedType.U4)] int cwcModifier,
      [MarshalAs(UnmanagedType.U4)] int ulAttachmentType);
     void PutPhrase([MarshalAs(UnmanagedType.LPWStr)] string pwcPhrase,
      [MarshalAs(UnmanagedType.U4)] int cwcPhrase);
    }
    [ComImport]
    [Guid("fe77c330-7f42-11ce-be57-00aa0051fe20")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IWordFormSink {
     void PutAltWord([MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf, [MarshalAs(UnmanagedType.U4)] int cwc);
     void PutWord([MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf, [MarshalAs(UnmanagedType.U4)] int cwc);
    }
    
    [StructLayout(LayoutKind.Sequential)]
    public struct TEXT_SOURCE {
     [MarshalAs(UnmanagedType.FunctionPtr)]
     public delFillTextBuffer pfnFillTextBuffer;
     [MarshalAs(UnmanagedType.LPWStr)]
     public string awcBuffer;
     [MarshalAs(UnmanagedType.U4)]
     public int iEnd;
     [MarshalAs(UnmanagedType.U4)]
     public int iCur;
    }
    
    // used to fill the buffer for TEXT_SOURCE
    public delegate uint delFillTextBuffer([MarshalAs(UnmanagedType.Struct)]
     ref TEXT_SOURCE pTextSource);
    
    [ComImport]
    [Guid("D53552C8-77E3-101A-B552-08002B33B0E6")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IWordBreaker {
     void Init([MarshalAs(UnmanagedType.Bool)] bool fQuery,
      [MarshalAs(UnmanagedType.U4)] int maxTokenSize,
      [MarshalAs(UnmanagedType.Bool)] out bool pfLicense);
     void BreakText([MarshalAs(UnmanagedType.Struct)] ref TEXT_SOURCE pTextSource,
      [MarshalAs(UnmanagedType.Interface)] IWordSink pWordSink,
      [MarshalAs(UnmanagedType.Interface)] IPhraseSink pPhraseSink);
     void GetLicenseToUse([MarshalAs(UnmanagedType.LPWStr)] out string ppwcsLicense);
    }
    
    [ComImport]
    [Guid("EFBAF140-7F42-11CE-BE57-00AA0051FE20")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface IStemmer {
     void Init([MarshalAs(UnmanagedType.U4)] int ulMaxTokenSize, [MarshalAs(UnmanagedType.Bool)] out bool pfLicense);
     void GenerateWordForms([MarshalAs(UnmanagedType.LPWStr)] string pwcInBuf, 
      [MarshalAs(UnmanagedType.U4)] int cwc, 
      [MarshalAs(UnmanagedType.Interface)] IWordFormSink pStemSink);
     void GetLicenseToUse([MarshalAs(UnmanagedType.LPWStr)] out string ppwcsLicense);
    }
    
    // Word breaker to break a line of text into words, stem them, 
    // and return a HashSet of all the words and stems
    public class WordBreaker : IWordSink, IWordFormSink {
     IWordBreaker wordBreaker;
     HashSet words; // Words to search for
     IStemmer istm;
     
     public WordBreaker() {
      wordBreaker = (IWordBreaker)ComHelper.CreateInstance(langwrbk, breaker);
      bool pfLicense = true;
      wordBreaker.Init(true, 1000, out pfLicense);
      istm = (IStemmer)ComHelper.CreateInstance(infosoft, stemmer);
      pfLicense = true;
      istm.Init(1000, out pfLicense);
     }
     
     public HashSet Search(string text) {
      words = new HashSet();
      TEXT_SOURCE pTextSource = new TEXT_SOURCE();
      pTextSource.pfnFillTextBuffer = new delFillTextBuffer(pfnFillTextBuffer);
      pTextSource.awcBuffer = text.ToLower();
      pTextSource.iCur = 0;
      pTextSource.iEnd = text.Length;
      wordBreaker.BreakText(ref pTextSource, (IWordSink)this, null);
      return words;
     }
     
     #region IWordFormSink Members
     public void PutAltWord(string pwcInBuf, int cwc) {
      words.Add(pwcInBuf.Substring(0, cwc));
     }
    
     public void PutWord(string pwcInBuf, int cwc) {
      words.Add(pwcInBuf.Substring(0, cwc));
     }
     #endregion
    
     #region IWordSink Members
     public void PutWord(int cwc, string pwcInBuf, int cwcSrcLen, int cwcSrcPos) {
      istm.GenerateWordForms(pwcInBuf.Substring(0, cwc), cwc, this);
     }
     
     public void PutAltWord(int cwc, string pwcInBuf, int cwcSrcLen, int cwcSrcPos) {
     }
    
     public void StartAltPhrase() {
     }
     
     public void EndAltPhrase() {
     }
    
     public void PutBreak(WORDREP_BREAK_TYPE breakType) {
     }
     #endregion
    }
    
    static uint pfnFillTextBuffer(ref TEXT_SOURCE pTextSource) {
     // return WBREAK_E_END_OF_TEXT
     return 0x80041780;
    }
    
    // Code from https://gist.github.com/1568627
    // By John Jeffery
    static class ComHelper
    {
     private delegate int DllGetClassObject(ref Guid clsid, ref Guid iid, [Out, MarshalAs(UnmanagedType.Interface)] out IClassFactory classFactory);
    
     public static object CreateInstance(LibraryModule libraryModule, Guid clsid)
     {
      var classFactory = GetClassFactory(libraryModule, clsid);
      var iid = new Guid("00000000-0000-0000-C000-000000000046"); // IUnknown
      object obj;
      classFactory.CreateInstance(null, ref iid, out obj);
      return obj;
     }
    
     static IClassFactory GetClassFactory(LibraryModule libraryModule, Guid clsid)
     {
      IntPtr ptr = libraryModule.GetProcAddress("DllGetClassObject");
      var callback = (DllGetClassObject) Marshal.GetDelegateForFunctionPointer(ptr, typeof (DllGetClassObject));
    
      var classFactoryIid = new Guid("00000001-0000-0000-c000-000000000046");
      IClassFactory classFactory;
      var hresult = callback(ref clsid, ref classFactoryIid, out classFactory);
    
      if (hresult != 0)
      {
       throw new Win32Exception(hresult, "Cannot create class factory");
      }
      return classFactory;
     }
    }
    
    
    [Guid("00000001-0000-0000-c000-000000000046")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    [ComImport]
    interface IClassFactory
    {
     void CreateInstance([MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppvObject);
     void LockServer(bool fLock);
    }
    
    class LibraryModule : IDisposable
    {
     private readonly IntPtr _handle;
     private readonly string _filePath;
    
     private static class Win32
     {
      [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
      public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
    
      [DllImport("kernel32.dll")]
      public static extern bool FreeLibrary(IntPtr hModule);
    
      [DllImport("kernel32.dll", SetLastError = true)]
      public static extern IntPtr LoadLibrary(string lpFileName);
     }
    
    
     public static LibraryModule LoadModule(string filePath)
     {
      var libraryModule = new LibraryModule(Win32.LoadLibrary(filePath), filePath);
      if (libraryModule._handle == IntPtr.Zero)
      {
       int error = Marshal.GetLastWin32Error();
       throw new Win32Exception(error, "Cannot load library: " + filePath);
      }
    
      return libraryModule;
     }
    
     private LibraryModule(IntPtr handle, string filePath)
     {
      _filePath = filePath;
      _handle = handle;
     }
    
     ~LibraryModule()
     {
      if (_handle != IntPtr.Zero)
      {
       Win32.FreeLibrary(_handle);
      }
     }
    
     public void Dispose()
     {
      if (_handle != IntPtr.Zero)
      {
       Win32.FreeLibrary(_handle);
      }
      GC.SuppressFinalize(this);
     }
    
     public IntPtr GetProcAddress(string name)
     {
      IntPtr ptr = Win32.GetProcAddress(_handle, "DllGetClassObject");
      if (ptr == IntPtr.Zero)
      {
       int error = Marshal.GetLastWin32Error();
       string message = string.Format("Cannot find proc {0} in {1}", name, _filePath);
       throw new Win32Exception(error, message);
      }
      return ptr;
     }
    
     public string FilePath
     {
      get { return _filePath; }
     }
    }