QBASIC-to-PowerBasic Console Compiler porting notes
September 17, 2005 (as amended)

Why port from QBASIC to PB/CC?

  1. QBASIC is a very basic editor which suffers from memory constraints and lack of modern features, including a cut-and-paste that is easily compatible with Windows
  2. programs written in QBASIC and then compiled with PowerBASIC for DOS are still 16-bit console apps, and thus suffer from various compatibility problems, particularly with Windows 2000 and XP, including access to LFNs.
  3. full access to all of Windows' memory, and the Windows API is available under PB/CC
  4. the PB/CC version of a program seems to run between 2 and 5 times faster
  5. PB/CC has some cool new keywords!

So basically if your QBASIC programs have grown too large, you need to write LFNs, you want one set of code to work on your various 9X/NT/2K/XP boxes, and/or, you want access to loads of memory and the full Windows API including TCP/IP, and/or, you want more speed, PB/CC is the way to go.

During porting, you might consider rewriting some code as functions and parameters, or making other tweaks to take advantage of PB/CC's power. However you might also consider leaving this until your port is stable.

QBASIC -> PB/CC porting process

  1. Enclose program header in PBMAIN structure:

    Function PBMain () As Long
    ...
    End Function
    
  2. Convert all fullstops in variablenames to underscores; PB/CC does not support periods in variablenames. Search-and-replace on the following:

    a. b. c. d. e. f. g. h. i. j. k. l. m. n. o. p. q. r. s. t. u. v. w. x. y. z.

    Note: this kind of search-and-replace will break any hardcoded filenames and URLs you have in your code; file.txt would be converted to file_txt, while www.yahoo.com would be converted to www_yahoo_com. A safer but slower search-and-replace might search on "d.msg$" and replace with "d_msg$", or search on "split." and replace with "split_".

  3. Convert all dashes in variablenames to underscores; PB/CC does not support hyphens in variablenames. Search-and-replace on the following:

    a- b- c- d- e- f- g- h- i- j- k- l- m- n- o- p- q- r- s- t- u- v- w- x- y- z-

    Note: this kind of search-and-replace will break any hyphenated text you have in your code; Tic-Tac-Toe would be converted to Tic_Tac_Toe. A safer but slower search-and-replace might search on "d-msg$" and replace with "d_msg$", or search on "split-" and replace with "split_".

  4. Convert all QBASIC subroutines defined like this:

    boot:
     a=b
     c=a+x
    RETURN
    

    to this:

    SUB boot
     a=b
     c=a+x
    END SUB
    

    Note: converting the subroutine to this syntax will scope all variables within the subroutine as local. Ensure to create GLOBAL statements for any variables within the subroutine that are intended to be globally scoped.

  5. Convert all calls to subroutines from GOSUB to CALL (do a search-and-replace, changing all strings "GOSUB " to "CALL "):

    GOSUB myroutine
    

    becomes:

    CALL myroutine
    

    Failing to change a GOSUB to a CALL will tell PB/CC to look for the subroutine in the local context (eg. within current subroutine). If the subroutine cannot be found in a local context, a compile-time error will be generated: "Undefined label/line reference".

  6. Convert all unsigned integer variables like this:

    x=4
    

    to this:

    x!=4    ----OR----    x%=4
    

    Use the ! type specifier if your code wants single-precision numbers; use % if your code only needs integers.

  7. Rewrite any global errorhandler you may have had as local code (this bit is a pain) - PB/CC does not support ON ERROR GOTO in a global context.

  8. Depending on the code, copy all DIM statements to a new set of GLOBAL statements, and move them (without the size of the arrays specified) to the top of the code. Convert the remaining DIM statements to PB/CC DIM statements and place them at the top of each SUB that uses those arrays. Like this:

    DIM infile$(p.size%)
    

    At the top of your QBASIC program would be copied to a statement in the PB/CC header:

    GLOBAL infile$()
    

    The DIM statement would then become:

    DIM infile(p_size%) AS GLOBAL STRING
    

    ...and placed at the top of each SUBroutine that uses the infile$() array.

    Doing this creates a global (shared) array.

    Failing to do this will produce runtime GPFs, when the program attempts to access the global array. Runtime GPFs may also occur if an attempt is made to access an array which is undimensioned. This may happen, for example with this code:

    DIM infile(p_size%) AS GLOBAL STRING
    

    ..however that code will only fail to dimension the array if p_size%=0. Which can happen if the DIM statement is before p_size%=100 (or whatever) in the code.

    Integer arrays (type specifier: %) should be declared like this:

    DIM altnum(26) As GLOBAL INTEGER
    

    Short integer arrays (type specifier: !) should be declared like this:

    DIM shortaltnum(26) As GLOBAL SINGLE
    

    Long integer arrays (type specifier: &) should be declared like this:

    DIM longaltnum(26) As GLOBAL LONG
    
  9. Depending on the code, declare all shared variables with GLOBAL statements at the top of the code, like this:

    GLOBAL p_size%, p_cfgsize%, p_bufsize%
    

    Failing to declare as GLOBAL variables you intended to be shared does not produce a runtime GPF (unlike unintentionally using an undimensioned array) - it simply scopes the variable in a local context. The consequence of this is that a routine may well execute successfully, however if the variables it uses to communicate success, failure and any other data back to its calling code are not declared as global, then the calling code will not be able to see the values of those variables. Instead the calling code will see its own local context of the variable, which will probably be empty, and will certainly not be what it was expecting. The symptom of an undeclared GLOBAL variable is thus a subroutine that seems to return nothing, or to leave a value unchanged. These seem to be the most difficult bugs to find, and speak volumes for the usefulness of use strict; and its equivalents (OPTION EXPLICT under PB/CC).

  10. Change any SHELL statements that use internal DOS commands such as DIR, SET or VER to use PB/CC's ENVIRON$ to call the command processor, as detailed at the bottom of the page in PB/CC's help entry for the SHELL statement. Like this:

    SHELL "SET > " + p.s$
    

    turns to:

    SHELL ENVIRON$("COMSPEC") + " /C SET > " + p_s$
    

    Some programs, particularly those using a commandline with redirection or piping, do not seem to execute correctly in a PB/CC SHELL - the redirection commands seem to be either lost or interpreted, rather than parsed by DOS. Programmatically generating a batchfile and executing that instead seems to fix this problem.

  11. Consider changing any INKEY$ loops you have to use WAITKEY$. The CPU load goes from around 100% to around 5% on this test machine. However, WAITKEY$ does wait - if your code needs to continue executing, if no keypress was made, add a SLEEP 0 to the end of the keyboard polling loop - this releases the timeslice to the rest of the CPU, and prevents the keyboard polling loop from consuming all of the CPU. Example:

    LOOP_EXIT=0
    WHILE LOOP_EXIT=0
     KEYPRESS$=INKEY$
     IF KEYPRESS$ > "" THEN
      [ ... code ... ]
     END IF
     SLEEP 0
    WEND
    
  12. Rewrite any code that was dependent upon running as 16-bit. In particular, code that reads directory listings will need to be rewritten if it is to continue to work on Windows NT/2K/XP after being ported from 16-bit to 32-bit. There are a number of 16/32 bit issues: more details here.

notable QBASIC and PB/CC differences

(This is a non-exhaustive list I encountered during my own porting experiences. Additional differences can be found in the PB/CC helpfile under Appendix B - Upgrading from DOS.)