Lesson's learned: Building an API wrapper library

2022-08-11

Planif v0.1.0 was recently released and with this release I thought I would speak about some of the lesson's I've learned along the way. Creating the library and covering all the existing triggers for Task Scheduler 2.0 took longer than I expected and I learned a lot about how the task scheduler works along the way.

Why create a wrapper?

This story starts with another of my rust projects, the price checker app. I was trying to decide if I wanted the app to run like a service, where it handles its own scheduling or use the OS's scheduler. I finally decided to use the OS's scheduler to save me some time and headache. I figured I would just go ahead and add and dependency on the windows-rs crate.

That's when I started looking at the windows-rs documentation. It was at around this point that the "save me a headache" flew right out the window. You see, the documentation for windows-rs is... bad. It looks as though the MS team took an existing API and 1 to 1 converted it for rust use however I can't really blame them. The windows API is massive (!) and adding any kind of documentation that probably doesn't exist in the first place would likely result in the windows-rs crate never getting released.

Which brings us to why I created the wrapper. The library is missing documentation and contains a lot of non-rust and more C++ like (Microsoft) naming conventions. If you have ever worked with .Net, you will recognise names like ITaskScheduler where interfaces are prefixed with I. Or maybe you'll notice BSTR or i16/i32 used for parameter values.

The goal of the wrapper is to create a builder to add a layer of abstraction between the Microsoft-y interfaces and objects and what you might expect to see in rust code. At the same time I also wanted to make sure the documentation was better.

windows-rs documentation is wanting

The windows-rs documentation is bad and again, I don't think the Microsoft teams could have added good (in this case any) documentation since the API is massive. The best documentation I could find was the Microsoft's Task Scheduler Reference. The docs contain C++ interfaces with a few examples and an XML schema.

It's safe to say that I don't know C++ well, but I was able to use the docs (including the schema) to decipher the majority of information to create the library. The docs weren't exactly flawless, some information was hidden in corners that were easy to miss. For example the Start Boundary being required on Calendar events (the requirement isn't specified on the trigger descriptions themselves but in a generic Calendar section).

The is also a weird behaviour in that the SetDayOfMonth method takes an i32 and the docs contain an option to specify last (as in last day of month) which causes the i32 to overflow. After testing, this seems to be expected behaviour.

Write documentation and tests (examples) as you go

On the subject of documentation, write it as you go. In rust, this also means create your examples and test them as you go as well. I tried being lazy and ended up copying examples and changing to fit it to the function in question but then when you have 1 error in an example, you will have the same error in all examples.

Although I didn't use it in the library's examples, I also learned that you can prepend example lines with # which will hide the line from the example. Meaning the example can focus on what's important while still compiling.

Implementations in another language is a great way to get started

By the time I decided to take on this project, I had found a pre-existing rust library by jc-h on github. Their project helped me understand how to grok windows-rs by cross referencing the code on github and Microsoft's C++ examples.

After that I was quickly able to use the information in the Task Scheduler Reference to create all the remaining triggers.

What's next

There are still a few improvements before planif reaches v1.0 including a better (nicer) README.md and adding more examples in /examples/. It would also be nice to use the anyhow or thiserror for the custom errors I've created in the library.

In the mean time, all the triggers are currently supported by the library and is usable. However the adding of the error libraries would likely create a breaking change.