VB6 vs VB.NET language - Language

Unless otherwise stated, VB Migration Partner fully supports all the Visual Basic 6 features and keywords mentioned in this page. For more information, please read the manual and the knowledge base section.


Integer data types

A VB6 Integer variable is a 16-bit signed integer, therefore it should be translated to Short under VB.NET; likewise, a VB6 Long variable is a 32-bit signed integer and should be translated to Integer under VB.NET. (A VB.NET Long variable is a 64-bit signed integer.)

Currency data type

The Currency data type isn’t supported by VB.NET; variables of this type should be converted to Decimal. However, the Decimal data type has greater precision and range than Currency, therefore you have no guarantee that math expressions deliver the same result they do in VB6. For example, in VB6 a Currency operation might raise an overflow, but it would be evaluated correctly under VB.NET.

Variant data type

VB.NET doesn’t support the Variant data type; all Variant members are translated to Object members. In many cases, however, the two types aren’t equivalent. For example, an Object .NET variable can’t hold the special Empty, Null, and Missing values.

VB Migration Partner offers the option to render a Variant variable with the special VB6Variant type, if a SetType or ChangeType pragma is specified.

NOTE: starting with version 1.52, the VB6Variant class is not officially supported.

Type declaration suffixes

A consequence of the fact that VB.NET redefines the meaning of Integer and Long keywords is that the meaning of type declaration suffixes has changed too. Now a variable whose name ends with “%” is a 32-bit integer; a variable whose name ends with “&” is a 64-bit integer; a variable whose name ends with “@” is a Decimal variable. However, it is recommended that you get rid of type declaration suffixes and convert them in standard and more readable As clauses.

Fixed-length strings

VB6 fields, local variables, and members of Type structures can be defined as fixed-length strings, as in:
        Dim buffer As String * 256
An uninitialized fixed-length string initially contains only ASCII 0 characters; when you assign any value to it, the value is truncated to the maximum length, or padded with spaces to the right if shorted than the maximum length. VB.NET doesn’t support fixed-length strings. The Microsoft.VisualBasic.Compatibility.dll assembly defines a FixedLengthString type which behaves like fixed-length strings, but there are significant differences with the original VB6 type.

VB Migration Partner maps fixed-length strings to the VB6FixedString type, which mimics VB6 behavior more closely:
        Dim buffer As VB6FixedString(256)

Conversions between Date and Double types

VB6 allows you to use a Double variable whenever a Date value is expected, and vice versa. This is possible because a Date value is nothing but a Double value whose integer portion represents the number of days elapsed since December 30, 1899 and whose fractional part represents the time portion of the date. When converting a piece of VB6 to VB.NET such implicit conversions must become explicit by calls to the ToOADate and FromOADate methods of the Date type:
        Dim dat As Date, dbl As Double
        dbl = dat.ToOADate()
        dat = Date.FromOADate(dbl)
For uniformity and readability’s sake, VB Migration Partner generates calls to DateToDouble6 and DoubleToDate6 methods when the original VB6 code implicitly converts a Date value to Double, or vice versa.

Conversions between String and Byte arrays

VB6 supports implicit conversions from String to Byte arrays, and vice versa, as in this code snippet:
        Dim s1 As String, s2 As String, b() As Byte
        s1 = "abcde"
        b = s1
        s2 = b
VB.NET doesn’t support such implicit conversions and requires explicit calls to methods of the System.Text.Encoding class:
        Dim s1 As String, s2 As String, b() As Byte
        s1 = "abcde"
        b = Encoding.Unicode.GetBytes(s1)
        s2 = Enconding.Unicode.GetString(b)
For uniformity and readability’s sake, VB Migration Partner generates calls to ByteArrayToString6 and StringToByteArray6 methods when the original code implicitly converts a Byte array to a String, or vice versa.

Conversions from Boolean values

VB6 supports implicit conversions from Boolan values to other numeric data types. VB.NET requires that you use the appropriate conversion operator.

VB Migration Partner uses the CByte operator when converting to a Byte variable and CShort when converting to any other numeric data type.

VB.NET keywords

Several VB.NET keywords aren’t reserved words under VB6 and can be used as member names. Examples are AddHandler, Handles, Shadows, and TimeSpan. When the name of a VB6 member matches a VB.NET keyword it must be enclosed between square brackets, as in
        Dim [handles] As Integer

Block variables

If Dim keyword appears inside an If, For, For Each, Do, While, or Select block, then VB2005 limits the scope of the variable to the block itself whereas VB6 makes the variable visible to the entire method:
        Sub Test(ByVal n As Integer)
            If n > 0 Then
                ' in VB.NET the x variable can be referenced only from by the code
                ' between the If and the Else keywords.
                Dim x As Integer
            End If
        End Sub
VB Migration Partner automatically moves the variable declaration outside the code block:
        Sub Test(ByVal n As Short)
            Dim x As Short
            If n > 0 Then
            End If
        End Sub

Auto-instancing variables

VB6 variables declared with the “As New” clause are known as auto-instancing variables. The key property of these variables is lazy instantiation: the object referenced by the variable is created as soon as one of its members is referenced; if the variable is set to Nothing, the object is re-instantiated the next time the variable is referenced. (A side-effect of this behavior is that testing such a variable against Nothing always returns False.) The .NET Framework has no notion of auto-instancing variables; in fact the following VB.NET statement
        Dim w As New Widget
is just a shorthand for the following, more verbose, declaration:
        Dim w As Widget = New Widget
where it is clear that the object is instantiated when it is declared. If you later set the variable to Nothing, no object is re-created when you reference the variable again.

By default, VB Migration Partner translates auto-instancing variables verbatim, therefore the original VB6 semantics is lost and runtime errors might occur in the converted program. However, it offers the ability to generate code that preserves the VB6 behavior and avoids subtle bugs or unexpected exceptions. You can enable this feature by means of the AutoNew pragma.

Auto-instancing arrays

In addition to individual auto-instancing variables, VB6 also supports auto-instancing arrays, as in the following statement:
        Dim arr(10) As New Widget
Each element of such an array behaves like an auto-instancing variable. However, the “As New” clause is invalid for VB.NET arrays, therefore VB Migration Partner drops the “New” keyword and generate a regular array:
        Dim arr(10) As Widget
It is up to the developer to correctly initialize all the elements in the array to avoid NullReference exceptions. However, if the application relies on the auto-instancing semantics, such a fix causes the VB.NET application to behave differently from the original VB6 code. VB Migration Partner offers the ability to create a “true” auto-instancing array, by means of the AutoNew pragma.

Parameter default passing mechanism

Under VB6, method parameters are passed by-reference if the method parameter isn’t explicitly declared with the ByVal keyword. Under VB.NET, method parameters are passed by-value if the method parameter isn’t explicitly declared with the ByRef keyword. Both VB Upgrade Wizard and VB Migration Partner correctly add an explicit ByRef for parameters that don’t specify a ByVal keyword.

Additionally, VB Migration Partner detects parameters that unnecessarily use ByRef and can optional convert them to ByVal parameters.

Optional parameters

In VB6 you can include or omit the default value of an optional parameter; if you omit it, the default value for its type is assumed (0 for numeric types, “” for strings, Nothing for objects):
        Sub Test(ByVal Optional x As Short, ByVal Optional y As String)
            ' ...
        End Sub
VB.NET requires that the default value for a parameter be specified:
        Sub Test(ByVal Optional x As Integer = 0, ByVal Optional y As String = "")
            ' ...
        End Sub

ParamArray parameters

VB6 requires that ParamArray parameters be specified with an implicit ByRef keyword:
        Sub Test(ParamArray arr() As Variant)
            ' ...
        End Sub
By contrast, VB.NET requires that an explicit ByVal keyword be specified:
        Sub Test(ByVal ParamArray arr() As Object)
            ' ...
        End Sub
This detail makes the difference if the method modifies one of the elements of the parameter and some code relies on the fact that the argument is modified, as in this code:
    Sub Increment(ParamArray arr() As Variant)
        Dim i As Integer
        For i = 0 To UBound(arr)
            arr(i) = arr(i) + 1
    End Sub
    Sub Main()
        Dim n As Variant
        n = 10
        Increment n
        Debug.Print n   ' => displays "11"
    End Sub
After the migration to VB.NET, individual values passed to a ParamArray parameter are passed by value, therefore any change inside the method isn’t propagated back to the caller.
    Sub Increment(ByVal ParamArray arr() As Object)
        Dim i As Short
        For i = 0 To UBound(arr)
            arr(i) = arr(i) + 1
    End Sub
    Sub Main()
        Dim n As Object
        n = 10
        Debug.WriteLine(n)  ' => displays "10"
    End Sub
VB Migration Partner copes with this issue in two ways. First, it makes you aware of the potential problem by emitting a warning if any element of a ParamArray vector is modified inside the method; second, it provides a pragma that allows you to generate an overload of the method that doesn’t suffer from the issue.

Assignments between arrays

When you assign an array to another array variable under VB6, a copy of the source array is assigned to the target variable. If you later modify either the source or the target array, the other array isn’t affected. VB.NET arrays are reference types, therefore assignment between arrays are resolved internally by just assigning the target variable a pointer to the source array. If you later modify either array, the other array is modified.
        Dim a(10) As Integer
        Dim b() As Integer
        b = a           ' copy the array
        a(0) = 999      ' the modify the source array
        MsgBox b(0)     ' displays '0' in VB6, '999' in VB.NET
VB Migration Partner solves this problem by invoking the destination array’s Clone method:
        targetArray = sourceArray.Clone()

Assignments between Structures

Both VB6’s Type blocks and VB.NET Structure blocks are value types; this implies that when you assign a Structure to a variable of same type, then a copy of the entire structure is assigned. If you later modify either the destination or the target Structure, then the other Structure isn’t affected in any way. However, in VB.NET there’s a caveat: if the Structure contains one or more arrays or fixed-length strings, then the target Structure shares a reference to these arrays or fixed-length strings. For example, consider the following code:
        Structure TestUDT
            Public Names() As String
            Public Location As VB6FixedString
        End Structure
        Dim source As TestUDT
        ReDim source.Names(10)
        source.Location = "Italy"
        ' assignment to a variable of same type
        Dim dest As TestUDT = source
        ' modify an array element in source UDT
        source.Names(1) = "Code Architects"
        ' check that the value is modified also in the other UDT
        Debug.WriteLine(dest.Names(1))         ' displays "Code Architects"
To have Structure assignments behave exactly as in VB6, if a Structure includes arrays or fixed-length strings then VB Migration Partner expands the Structure definition with a Clone method that returns a distinct copy of the Structure. All assignments between Structures of such types are then modified to call the Clone method:
        ' VB Migration Partner generates this code for UDT assignments
        Dim dest As TestUDT = source.Clone()

Structure initialization

If you declare a VB6 Type variable, all the elements in the Type are correctly initialized; a VB.NET Structure can include neither a default constructor nor field initializers, therefore a Structure variable that has been just declared can have one or more uninitialized fields. For example, consider the following VB6 Type block:
        Type TestUDT
            n As Integer
            s As String * 10
            a(10) As String
            w As New Widget
        End Type
and now consider the corresponding VB.NET Structure:
        Structure TestUDT
            Public n As Integer
            Public s As VBFixedString
            Public a() As String
            Public w As Widget
            Public Sub InitializeUDT()
                s = New VBFixedString(10)
                ReDim a(10)
                w = New Widget
            End Sub
        End Structure
The InitializeUDT method is necessary because .NET Structures can’t have constructors with zero arguments or field initializers. The Upgrade Wizard requires that you manually invoke the InitializeUDT method to ensure that a structure variable be correctly initialized before being used:
        Dim udt As TestUDT
        udt.InitializeUDT    ' you must insert this statement manually
VB Migration Partner frees you from the need to manually initialize the structure, because it generates a constructor with one (dummy) parameter and automatically invokes this constructor whenever the application defines a structure variable that requires this treatment:
        Structure TestUDT
            Public n As Integer
            Public s As VBFixedString
            Public a() As String
            Public w As Widget

            Public Sub InitializeUDT()
                s = New VBFixedString(10)
                ReDim a(10)
                w = New Widget
            End Sub

            Public Sub New(ByVal dummy As Boolean)
            End Sub
        End Structure
        ' this is the code generated when a TestUDT variable is declared
        Dim udt As New TestUDT(True)

Method calls

VB.NET requires that the list of arguments passed to a Sub method be always enclosed in parenthesis; in VB6 only calls that return a value require that argument list be enclosed in parenthesis:
        TestSub(12, "abc")

Late-bound method calls

VB.NET supports late-bound calls, but requires that the Option Strict Off directive be declared at the project-level or at the top of current file.

VB Migration Partner declares Option Strict Off at the top of each file. After the migration process you should attempt to drop these statements where possible, adjusting the code in the file as necessary.

Fields passed by reference to a method

Consider the following VB6 code, inside the Widget class:
        Public ID As String
        Public Function GetString() As String
            Dim widget As New Widget
            widget.ID = "abcde"
            TestMethod widget.ID
            GetString = widget.ID
        End Function
        Sub Test(ByRef text As String)
            text = UCase(text)
        End Sub
Invoking the GetString method under VB6 delivers the result “abcde”, which demonstrates that the ID field has been passed to Test method using by-value semantics even if the receiving text parameter is declared with the ByRef keyword. This behavior can be explained by knowing that a VB6 field is actually compiled as a Property Get/Let pair and therefore the Test method is actually receiving the result of the call to the “setter” block, not the actual field.

When this code is converted “as-is” to VB.NET, the ID field is uppercased on return from the Test method, which proves that VB.NET differs from VB6 in how fields are handled.

VB Migration Partner detects the potential bug and emits code that enforces the by-value semantics. The Upgrade Wizard converts the code as-is and doesn’t emit a warning in this case.

Uninitialized local variables

If a local variable of reference type – that is, a String or Object variable – is declared and not immediately initialized, the VB.NET compiler emits the following warning:
        Variable 'varname' is used before it has been assigned a value. 
        A null reference exception could result at runtime.
To avoid this warning you should initialize the variable within the Dim statement:
        Dim text As String = ""
        Dim obj As Object = Nothing

References to methods defined in modules

If code inside a VB6 form invokes a method defined in a BAS module and the base System.Windows.Forms.Form class exposes a public or protected method with same name, then a compilation error occurs (if the two methods have different syntax) or, worse, the program might not work as intended and possibly throw unexpected exceptions at runtime. For example, suppose that the following statements are located inside a VB6 form:
        ' both these methods are defined in Helpers.bas module
The PerformLayout method is exposed by the .NET System.Windows.Forms.Form class, but it has a different syntax and therefore it is marked under VB.NET as a compilation error. The ProcessKeyDialog method is also exposed by the .NET Form class and it takes a character as an argument. Consequently, the form’s ProcessKeyDialog method is invoked instead of the method defined in the Helpers.bas module, which surely causes a malfunctioning. In both cases, you can resolve the ambiguity by prefixing the method with the module’s name (VB Migration Partner applies this fix):
VB Migration Partner detects method calls that are ambiguous and generates code that behaves as intended.

Event handlers

In VB6 a method that handles an event must follow the object_eventname naming convention. In VB.NET event handlers can have any name, provided that they are marked with an opportune Handles clause:
        Private Sub NameClickHandler(ByVal sender As Object, ByVal e As EventArgs) _
            Handles txtName.Click
            ' handle click events originating from the txtName control
            ' ...
        End Sub
Notice that the Handles clause for events raised by the form itself must reference the MyBase object:
        Private Sub FormClickHandler(ByVal sender As Object, ByVal e As EventArgs) _
            Handles MyBase.Click
            ' handle click events originating from the current form
            ' ...
        End Sub

Name collisions for Type…End Type blocks

In VB6 it is legal to have a private Type…End Type block with same name as a public class or as a Declare method defined elsewhere in the project. When the code is converted to VB.NET, the Structure that corresponds to the original Type must be renamed to avoid these name collisions.

Sub Main

When a Sub Main method is converted to VB.NET it should be decorated with an STAThread attribute:
        <STAThread()> _
        Public Sub Main()
            ' ...
        End Sub

Member shadowing

A method, property, or event defined in a VB6 form might coincidentally have same name as a member exposed by the System.Windows.Forms.Form class, for example:
        Sub PerformLayout(ByVal refresh As Boolean)
            ' ...
        End Sub
When this code is converted and compiled under VB.NET a warning occurs, because the .NET Form class exposes a method named PerformLayout. To make the compiler happy you should add a Shadows keyword:
        Shadows Sub PerformLayout(ByVal refresh As Boolean)
            ' ...
        End Sub
VB Migration Partner automatically applies this fix when necessary.

Null propagation

VB6 Variant variables can hold the special Null value, but no corresponding value exists in VB.NET. What makes matters worse is that several VB6 functions – namely Str, Hex, Oct, CurDir, Environ, Chr, LCase, UCase, Left, Mid, Right, Trim, RTrim, LTrim, and Space – support null propagation, as the following VB6 code demonstrates:
        Dim var As Variant
        var = Null
        var = var + 10        ' var is assigned Null, no error is raised
        var = Left(var, 1)    ' var is assigned Null, no error is raised
A Null value is neither True nor False, therefore when the test condition of an If block is evaluated as Null, then the Else blocks is always executed; prefixing the expression with the Not operator doesn’t transform the expression into a non-Null value. You often need to manually fixing converted VB6 code that relies on the peculiar way in which VB6 deals with Null values.

A Null value is often the result of a read operation from a database field that contains the NULL value. When converted to VB.NET, the actual value stored in the variable is DBNull.Value, but the two values aren’t equivalent. For example, an exception is thrown at runtime if the test condition in an If statement evaluates to DBNull.Value, whereas no runtime error occurs in VB6 if the test condition of an If statement evaluates to Null.

VB Migration Partner offers a partial solution to the null propagation problem thanks to the special VB6Variant type and the NullSupport pragma.

Enum member names

VB6 allows you to use virtually any character inside the name of an Enum member. If the name isn’t a valid Visual Basic identifier you just need to enclose the name inside square brackets:
        Public Enum Test
            [two words]          ' space
            [dollar$ symbol]     ' symbol and space
            [3D]                 ' leading digit
            [New]                ' language keyword
        End Enum
VB.NET forbids Enum member’s names that start with a digit or contain spaces or other symbols. VB.NET does support square brackets in Enum names, but they only allow to define names that match a language’s keyword.

VB Migration Partner handles this situation by replacing invalid characters with underscores and using a leading underscore if the first character is a digit:
        Public Enum Test
        End Enum

References to enum members

In VB6 the name of an enum member is considered as a global name. For example, you can reference members of the ColorConstants enum type with or without including the enum name:
        txtName.BackColor = ColorContants.vbYellow
        txtName.BackColor = vbYellow
In VB.NET the name of the enum type can’t be omitted, therefore only the first syntax form is valid.

Date variables in For…Next loops

VB6 supports Date variables as controlling variables in For…Next loops:
        Dim d As Date
        For d = Date To Date + 10
            ' …
Date variables can’t be used as controlling variables in VB.NET For…Next blocks, therefore VB Migration Partner converts the above code by using an “alias” variable of type Double:
        Dim d As Date
        For d_Alias As Double = DateToDouble6(Date) To DateToDouble6(Date + 10)
            d = DoubleToDate6(d_Alias)
            ' …

Multi-dimensional arrays in For Each…Next loops

There is a minor difference in how elements of a multi-dimensional array are accessed when the array appears in a For Each…Next loop:
        Dim arr(10, 20) As Double
        Dim v As Variant
        For Each v In arr
Under VB6, elements are accessed in column-wise order – that is, first all the elements of first column, then all elements in second column, and so forth. Conversely, under VB.NET the elements are accessed in row-wise fashion – that is, first all the elements of the first row, then all the elements of second row, and so forth. If visiting order is significant, the same loop delivers different results after the migration to VB.NET.
VB Migration Partner emits a warning when a multi-dimensional array appears in a For Each…Next block. If you believe that preserving the original visiting order is important, you can insert a call to the TransposeArray6 method (defined in VBMigrationPartner_Support module), which transposes array elements so that the migrated code works as the original one:
        For Each v In TransposeArray6(arr)

File operations with UDTs

VB6 and VB.NET greatly differ in how UDTs - Structures in VB.NET parlance – are read from or written to files. Not only are structure elements stored to file in a different format, but the two languages also manage the End-of-File condition in a different way. In VB6 you can read a UDT even if the operation would move the file pointer beyond the current file’s length; in VB.NET such an operation would cause an exception.

VB Migration Partner correctly handles both these problems, by generating code that invoke alternate file-handling methods, such as FileGet6 and FilePut6.

Collections can’t be modified inside For Each…Next loops

VB6 allows you to modify a collection inside a For Each…Next loop that iterates on the collection itself. For example, the following code works well and is indeed quite common in VB6:
        Dim frm As Form
        For Each frm In Forms
            Unload frm
This code throws an exception under VB.NET, because unloading a form causes the Forms collection to change inside the loop. The simplest way to work around this problem is having the loop iterate on a copy of the collection, as in this example:
        Dim values As New List(Of String)
        For Each item As String In New List(Of String)(values)
VB Migration Partner doesn’t generate this fix, because it is impossible to automatically detect whether the collection is indirectly modified by any method call inside the loop.

DAO.DBEngine object

The DAO.DBEngine object is a global object, which means that VB6 application can reference its members without having to instantiate it first and that it isn’t necessary to include the class name in the method call. In practice, this means that the following VB6 method calls to the OpenDatabase method are both valid and have the same effects:
        Dim db1 As DAO.Database, db2 As DAO.Database
        Set db1 = DBEngine.OpenDatabase("biblio.mdb")
        Set db2 = OpenDatabase("northwind.mdb")
VB.NET doesn’t support global objects, therefore the above statements must be converted as follows:
        Dim dbeng As New DAO.DBEngine
        Dim db1 As DAO.Database, db2 As DAO.Database
        Set db1 = dbeng.OpenDatabase("biblio.mdb")
        Set db2 = dbeng.OpenDatabase("northwind.mdb")

ByVal keyword in method calls

VB6 allows you to pass an argument to a by-reference parameter using by-value semantics, by prefixing the argument with the ByVal keyword:
        ' the 'address' variable is passed by value
        CopyMethod ByVal address, arr(0), 1024
This calling syntax can be used only with Declare methods, and is especially useful with methods whose arguments are declared As Any, because you can’t use the ByVal keyword in the declaration of “As Any” parameters. VB.NET supports neither the ByVal keyword in method call nor As Any parameters in Declare statements.

VB Migration Partner accounts for such ByVal keywords and generates the corresponding overload for the Declare method.

Declare statements pointing to Visual Basic runtime

Expert VB6 developers can invoke methods defined in VB6 runtime, for example to retrieve information about variables and arrays. Code that uses methods defined in the VB6 runtime can’t be migrated to VB.NET, both because the MSVBVM60.DLL library isn’t available and because.NET variables and arrays are stored differently from VB6 and therefore the methods wouldn’t work anyway because.

VB Migration Partner flags Declare statements that point to VB6 runtime with an appropriate warning.

Resource files

VB6 resource files can’t be used under VB.NET and should be converted separately. In addition to convert files to the .NET Framework format, VB.NET applications must be able to reference resources as My.Resources.Xxxx elements.

Image format tests

VB6 developers can test the type of an image by testing the picture’s Type property against the values of the PictureTypeConstants enumerated type, as in:
        If Picture1.Picture.Type = PictureTypeConstants.vbPicTypeEMetafile Then …
The .NET Image type doesn’t expose the Type property, but has an equivalent property named RawFormat.

Here’s how VB Migration Partner translates previous code:
        If Picture1.Picture.RawFormat is System.Drawing.Imaging.ImageFormat.Emf Then …

Remarks starting with three apostrophes

Many developers like to emphasize comments by creating lines of asterisks, dashes, or apostrophes:
        ' Methods
The problem in converting this code is that any remark that starts with three apostrophes is considered as an XML comment under VB.NET, therefore this code causes a compilation warning under VB.NET.

VB Migration Partner recognizes the problem and replaces the third apostrophe with a space:
        '' '''''''''''''''''''''
        ' Methods
        '' '''''''''''''''''''''

Follow Francesco Balena on VB6 migration’s group on


Read Microsoft Corp’s official case study of a VB6 conversion using VB Migration Partner.

Code Architects and its partners offers remote and onsite migration services.

More details

Subscribe to our free newsletter for useful VB6 migration tips and techniques.


To learn more about your VB6 applications, run VB6 Analyzer on your source code and send us the generated text file. You will receive a detailed report about your VB6 applications and how VB Migration Partner can help you to quickly and effectively migrate it to .NET.

Get free advice

A fully-working, time-limited Trial Edition of VB Migration Partner allows you to test it against your actual code

Get the Trial

The price of VB Migration Partner depends on the size of the VB6 application, the type of license, and other factors

Request a quote

Migrating a VB6 application in 10 easy steps

Comparing VB Migration Partner with Upgrade Wizard

Migration tools: Feature Comparison Table

All whitepapers