LDC 1.19 - Android, AVR. My rant on tests, update on JNI and COM.

Posted 2019-12-23

LDC 1.19 released with more android and AVR (the arduino chip) support built in. I rant on tests and update on JNI and COM.

Core D Development Statistics

In the community

Community announcements

See more at the announce forum.

What Adam is working on

I continued some work on my jni library, and revived my old COM library, renamed it, and tested it as a .net interop helper. I now have code that can read method definitions out of Java .class files, so soon I can auto-generate D bindings to Java APIs, including with overloading support. It may soon be possible to define new java classes in D as well. With this and the work LDC has done, official android support is right around the corner.

Meanwhile, encouraged by the beauty of the Java native interface code, I wanted to see if I could do something similar to .net. The answer is... sort of. The beginnings of my work can be seen here: http://dpldocs.info/experimental-docs/arsd.com.html and much more will be coming later.

There's three layers of COM: the low level one, directly calling IUnknown based interfaces, no assistance in memory management, no return value translations. D has this basically built in, though some helper functions can simplify it.

The middle layer calls the interfaces, but convert HRESULT returns to exceptions (and vice versa), out params to return values, and different levels of memory management. If you ask for a D type in a return value, it will copy it to the GC for convenience. If you ask for a wrapped type, it can put it in a RAII struct for more efficient, yet still assisted, access. I haven't completed most of this work yet.

The high level layer does all the translations of the mid-level, but through the IDispatch dynamic interface instead of through the function pointer interfaces. This brings a performance penalty, but is also convenient and widely compatible with the Windows ecosystem - this is how JScript and VBScript talk to other components (including yours!) and is well-supported by the .net runtime. This means you can load a .net object through COM and interact with it, even if you only know parts of the interface. Unlike the low-level interface, which requires all functions to be declared in proper order, you can use a dynamic dispatch to call just one individually declared method.

I like this kind of incremental, declare only what you use, interface because it eases experimentation and increases cross-version compatibility. You don't have to do a lot of up-front work translating the whole object when you only want a handful of methods.

Anyway, my goal with the new com.d is to allow all these layers, but right now it is biased toward the high level interface, since that is easiest to test. With C#, [ComVisible=true] is actually the default, but some VS-generated projects will set it to false in the assembly.cs file. Regardless, once it is true, you can sign your dll as you build it and register it in the registry then it is usable from D!

I am sure I'll be able to simplify it a little more in more time, but it already works if you follow the Windows rules. The other options for D to/from C# interaction tend to be:

  • Regular interprocess communication. Of course pipes or network protocols make talking between C# and D easy enough as is.
  • p/invoke on the C# side calling D through extern(C) interfaces.
  • Code generation on both sides may help with the above two techniques. The D projects autowrap and dflat take this approach.

And either or both of those approaches may be better for you, so it is possible this COM interop will not be that useful with C# per se, but since it is so common in the Windows ecosystem, I am sure we will find some value anyway eventually.

I'll keep working on this as time permits.

Adam's rant on testing

A test indicating a problem is not a failure; that's it doing exactly what it is supposed to do! On the other hand, a test is really failing when it doesn't detect a real bug, or throws up a complaint over nothing.

There's four general states tests can be in:

  • Detected a real bug. We often call this "test failed", but really, this is the test doing what it is supposed to do! I'd rather call it a "positive detection".
  • Detected a false bug. This is when the test "fails"... and here it is a true failure. The "test that cries wolf" wastes development time and erodes confidence in the testing system. I'd rather call this a "false positive".
  • Failed to detect a real bug. This means the test "passed" but the code was still broken. This is another case of test failure - it gives false confidence in the code, while simultaneously eroding confidence in the test system. Tests need to be written intelligently to avoid this failure mode. This is a "false negative".
  • Passed. The test correctly determined the code was bug free. This is difficult to believe in the real world because of the inherent difficulty to prove the absence of something you do not even necessarily know how to define.

To make more successful tests, I suggest the following:

  • Tests should be based on an outside authority of some sort. If you are implementing a spec, the tests should be independently based on that spec. If it is meant to be user facing, get actual users to try it and base your tests on what they did. Don't just repeat what you said in your implementation code in the test code. A bug *fix* should NEVER cause a test to fail - such indicates the test was too tightly coupled to the implementation to begin with.
  • Think about your tests and see that they cover edge cases. Think about possible categories of input rather than traditional code coverage targets or mere quantity of tests. Consider testing a function isOdd and only passing it values like 1, 3, 5, 7, 9, 11, etc. What about even numbers like 2? What about 0? What about negative numbers? 100% of your code may be covered, but only 1/4 of the input classes. Note that sometimes edge case inputs are not obvious - I suggest you refer to any number that is called out in the spec as something to consider.
  • Monitor false positives. If you see something frequently causing it, remove that test. It is better to have no test than a test that cries wolf.

I also personally loathe automated style checks. I get why people do them, but a space in the source code is not a bug and should not be treated as such. If you must do an automated style check, at least make sure it is in a different category than the other tests and do not block them, so a PR gets its substance tested even before the style is perfectly aligned.