2

EDIT#1 I am developing a VB6 EXE application intended to output some special graphics to the Adobe Illustrator.

The example code below draws the given figure in the Adobe Illustrator as a dashed polyline.

' Proconditions:
' ai_Doc As Illustrator.Document is an open AI document
' Point_Array represented as "array of array (0 to 1)" contains point coordinates
'
Private Sub Draw_AI_Path0(ByRef Point_Array As Variant)
Dim New_Path As Illustrator.PathItem
    Set New_Path = ai_Doc.PathItems.Add
    New_Path.SetEntirePath Point_Array
    New_Path.Stroked = True
    New_Path.StrokeDashes = Array(2, 1)
End Sub

This simple code, however, can raise a variety of run-time automation errors caused by:

  • Incorrect client code (for example, assigning a value other than Array to the New_Path.StrokeDashes)
  • Incorrect client data (for example, passing too large Point_Array to New_Path.SetEntirePath)
  • Unavailability of some server functions (for example when the current layer of the AI is locked)
  • Unexpected server behavior

EDIT#2

Unfortunately, since such errors are raised by the server app (AI, in our case) their descriptions are often inadequate, poor and misleading. The error conditions may depend on AI version, installed apps, system resources etc. A single problem can lead to different errors. Example passing too large Point_Array to New_Path.SetEntirePath (Windows XP SP3, Adobe Illustrator CS3):

  • For array size of 32767 and above, the error is -2147024809 (&H80070057) "Illegal Argument"
  • For array size of 32000 to 32766, the error is -2147212801 (&H800421FF) "cannot insert more segments in path. 8191 is maximum"

END OF EDIT#2

The traditional error handling can be used to prevent the client crash and to display the error details as shown below:

Private Sub Draw_AI_Path1(ByRef Point_Array As Variant)
Dim New_Path As Illustrator.PathItem
On Error GoTo PROCESS_ERROR
    Set New_Path = ai_Doc.PathItems.Add
    New_Path.SetEntirePath Point_Array
    New_Path.Stroked = True
    New_Path.StrokeDashes = Array(2, 1)
    Exit Sub
PROCESS_ERROR:
    MsgBox "Failed somewhere in Draw_AI_Path1 (" & Format(Err.Number) & ")" _
        & vbCrLf & Err.Description
End Sub

As you can see, the error number and error description can be accessed easily. However, I need to know also what call causes the error. This can be very useful for large and complex procedures containing many calls to the automation interface. So, I need to know:

  1. What error happened?
  2. What call caused it?
  3. In what client function it happened?

Objective #3 can be satisfied by techniques described here. So, let’s focus on objectives #1 and 2. For now, I can see two ways to detect the failed call:

1) To “instrument” each call to the automation interface by hardcoding the description:

Private Sub Draw_AI_Path2(ByRef Point_Array As Variant)
Dim New_Path As Illustrator.PathItem
Dim Proc As String
On Error GoTo PROCESS_ERROR
    Proc = "PathItems.Add"
    Set New_Path = ai_Doc.PathItems.Add
    Proc = "SetEntirePath"
    New_Path.SetEntirePath Point_Array
    Proc = "Stroked"
    New_Path.Stroked = True
    Proc = "StrokeDashes"
    New_Path.StrokeDashes = Array(2, 1)
    Exit Sub
PROCESS_ERROR:
    MsgBox "Failed " & Proc & " in Draw_AI_Path2 (" & Format(Err.Number) & ")" _
        & vbCrLf & Err.Description
End Sub

Weak points:

  • Code becomes larger and less readable
  • Incorrect cause can be specified due to copypasting

Strong points

  • Both objectives satisfied
  • Minimal processing speed impact

2) To “instrument” all calls together by designing a function that invokes any automation interface call:

Private Function Invoke( _
    ByRef Obj As Object, ByVal Proc As String, ByVal CallType As VbCallType, _
    ByVal Needs_Object_Return As Boolean, Optional ByRef Arg As Variant) _
    As Variant
On Error GoTo PROCESS_ERROR
    If (Needs_Object_Return) Then
        If (Not IsMissing(Arg)) Then
            Set Invoke = CallByName(Obj, Proc, CallType, Arg)
        Else
            Set Invoke = CallByName(Obj, Proc, CallType)
        End If
    Else
        If (Not IsMissing(Arg)) Then
            Invoke = CallByName(Obj, Proc, CallType, Arg)
        Else
            Invoke = CallByName(Obj, Proc, CallType)
        End If
    End If
    Exit Function
PROCESS_ERROR:
    MsgBox "Failed " & Proc & " in Draw_AI_Path3 (" & Format(Err.Number) & ")" _
        & vbCrLf & Err.Description
    If (Needs_Object_Return) Then
        Set Invoke = Nothing
    Else
        Invoke = Empty
    End If
End Function


Private Sub Draw_AI_Path3(ByRef Point_Array As Variant)
Dim Path_Items As Illustrator.PathItems
Dim New_Path As Illustrator.PathItem
    Set Path_Items = Invoke(ai_Doc, "PathItems", VbGet, True)
    Set New_Path = Invoke(Path_Items, "Add", VbMethod, True)
    Call Invoke(New_Path, "SetEntirePath", VbMethod, False, Point_Array)
    Call Invoke(New_Path, "Stroked", VbSet, False, True)
    Call Invoke(New_Path, "StrokeDashes", VbSet, False, Array(2, 1))
End Sub

Weak points:

  • Objective #1 is not satisfied since Automation error 440 is always raised by CallByName
  • Need to split expressions like PathItems.Add
  • Significant (up to 3x) processing speed drop for some types of automation interface calls

Strong points

  • Compact and easy readable code with no repeated on error statements

Is there other ways of handling automation errors?

Is there a workaround for the Weak point #1 for 2)?

Can the given code be improved?

Any idea is appreciated! Thanks in advance!

Serge

Community
  • 1
  • 1
Argut
  • 483
  • 3
  • 13
  • You certainly don't want to have tons of boilerplate code, or torture your code into something monstrous, just to do your error handling. What you want is a standard error handler, [something like this](http://stackoverflow.com/a/25739830/119775) — note the section dealing with unexpected errors. To figure out what caused an error, use the [Double Resume trick](http://stackoverflow.com/a/4432113/119775). You only really want that level of traceability during development — not during normal usage of a stable release. – Jean-François Corbett Dec 11 '14 at 20:53
  • @Jean-François, thanks! Analyzing the Error Code is perfect, and I will do it. However, this is not enough - please see my EDIT#2 – Argut Dec 12 '14 at 11:00
  • Right, and that's why I suggested the [Double Resume trick](http://stackoverflow.com/a/4432113/119775) above. Does this not work for you? – Jean-François Corbett Dec 12 '14 at 12:11
  • Double Resume works fine for debugging. However, I need error information when the compiled application runs – Argut Dec 16 '14 at 11:08

2 Answers2

2

Think of why it is you might want to know where an error has been raised from. One reason is for simple debugging purposes. Another, more important, reason is that you want to do something specific to handle specific errors when they occur.

The right solution for debugging really depends on the problem you're trying to solve. Simple Debug.Print statements might be all you need if this is a temporary bug hunt and you're working interactively. Your solution #1 is fine if you only have a few routines that you want granular error identification for, and you can tolerate having message boxes pop up. However, like you say, it's kind of tedious and error prone so it's a bad idea to make that into boilerplate or some kind of "standard practice".

But the real red flag here is your statement that you have "large and complex procedures containing many calls to the automation interface", plus a need to handle or at least track errors in a granular way. The solution to that is what it always is - break up your large and complex procedures into a set of simpler ones!

For example, you might have a routine that did something like:

Sub SetEntirePath(New_Path As Illustrator.PathItem, ByRef Point_Array As Variant)
    On Error Goto EH

    New_Path.SetEntirePath Point_Array

    Exit Sub

EH:

    'whatever you need to deal with "set entire path" errors
End Sub

You basically pull whatever would be line-by-line error handling in your large procedure into smaller, more-focused routines and call them. And you get the ability to "trace" your errors for free. (And if you have some kind of systematic tracing system such as the one I described here - https://stackoverflow.com/a/3792280/58845 - it fits right in.)

In fact, depending on your needs, you might wind up with a whole class just to "wrap" the methods of the library class you're using. This sort of thing is actually quite common when a library has an inconvenient interface for whatever reason.

What I would not do is your solution #2. That's basically warping your whole program just for the sake of finding out where errors occur. And I guarantee the "general purpose" Invoke will cause you problems later. You're much better off with something like:

Private Sub Draw_AI_Path4(ByRef Point_Array As Variant)

    ...

    path_wrapper.SetEntirePath Point_Array
    path_wrapper.Stroked = True
    path_wrapper.StrokeDashes = Array(2, 1)

    ...

End Sub

I probably wouldn't bother with a wrapper class just for debugging purposes. Again, the point of any wrapper, if you use one, is to solve some problem with the library interface. But a wrapper also makes debugging easier.

Community
  • 1
  • 1
jtolle
  • 7,023
  • 2
  • 28
  • 50
  • Thanks for the perfect answer, John! I thought about something like this before but I was afraid of a large amount of programming and performance issues. However, when I tried to implement the wrapper class for the PathItem lately I discovered no significant processing speed drop. A wrapper class localizes error processing fine. Also it benefits in early detection of pitfalls like [this](http://stackoverflow.com/questions/27482952/vb-assigning-to-a-boolean-property-in-adobe-illustrator-photoshop). All this results in increasing code safety. Many thanks again! – Argut Dec 15 '14 at 11:43
  • PS, sorry for not voting you up. As soon as I get enough reputation I'll do it! – Argut Dec 15 '14 at 11:46
1

One would run it in the VB6 debugger. If compiled without optimisation (you won't recognise your code if optimised) you can also get a stack trace from WinDbg or WER (use GFlags to set it up). HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug is where settings are stored.

You can also start in a debugger.

windbg or ntsd (ntsd is a console program and maybe installed). Both are also from Debugging Tools For Windows.

Download and install Debugging Tools for Windows

http://msdn.microsoft.com/en-us/windows/hardware/hh852363

Install the Windows SDK but just choose the debugging tools.

Create a folder called Symbols in C:\

Start Windbg. File menu - Symbol File Path and enter

srv*C:\symbols*http://msdl.microsoft.com/download/symbols

then

windbg -o -g -G c:\windows\system32\cmd.exe /k batfile.bat

You can press F12 to stop it and kb will show the call stack (g continues the program). If there's errors it will also stop and show them.

Type lm to list loaded modules, x *!* to list the symbols and bp symbolname to set a breakpoint

da displays the ascii data found at that address

dda displaysthe value of the pointer

kv 10 displays last 10 stack frames

lm list modules

x *!* list all functions in all modules

p Step

!sysinfo machineid

If programming in VB6 then this environmental variable link=/pdb:none stores the symbols in the dll rather than seperate files. Make sure you compile the program with No Optimisations and tick the box for Create Symbolic Debug Info. Both on the Compile tab in the Project's Properties.

Also CoClassSyms (microsoft.com/msj/0399/hood/hood0399.aspx) can make symbols from type libraries.

Community
  • 1
  • 1