For these module constructors and module destructors, it really should be noted with a big WARNING on their use. Because this code is run outside of user code, it's quite likely that fbc's error checking and some runtime facilities won't work as expected.
The result is that if an error occurs in the module constructor (implicit or explicit), that you can end up with the program aborting with no message even when '-exx' option for extra error checking is used. I think there was a discussion about module constructor / destructor priorities elsewhere on the forum and maybe this behaviour could be fixed up a little bit.
An alternate pattern to follow would be to do what the powerbasic users are doing and have only one module, and include all the sources. There's benefits to doing it that way, which I won't try to describe. I'll leave that to the pb experts.
A high level description of the start-up framework
1) At compile time make arrays of addresses for all the module constructors and destructors
2) At link time, store the arrays in the executable
3) At run time, the start-up framework will:
- call all the module constructors in the array
- call the main module user code
- on exit (or error), call all the module destructors (usually, depends on how the program failed though)
The start-up framework does other stuff too. What's important to note that the user (programmer) doesn't have a lot of control over the order in which constructors / destructors are executed. It's all handled by the start-up framework (and to some extent other compilers and the linker).
Overall, I think it would be good practice that module-level code only be in the main module. Currently, there's no compiler option that would enforce that, or for example require an explicit definition for the start of user code (aka main function).
For now though, I think should continue to document current state of fbc to give an understanding of it's workings. I don't really want to go way off on trying to fix everything that's wrong with fbc or what features that need to be added. I really just want to have some docs on the current state of fbc.
Next post I'll show an example where we can put the concept of module constructors & destructors in to user code with a few helper functions. I think this can provide an alternative method and also help explain what's going on under the hood when discussing the multiple modules and initializing and exiting code.
Executables and Compiling
Re: Executables and Compiling
So far as discussed in this topic, the module constructors / destructors are a low level feature of the start-up framework which involves gcc & c runtime code that executes before and after our usercode. We don't have much control over it.
Here we're going to build our own user code for constructors / destructors. We will still use the built-in module constructors / destructors (aka global ctors & dtors) but only to register the initialization and exit procedures (aka user ctors and dtors) in our user code. And we delay the execution of our procedures until we have control in main module user code.
To make this work, we need a little framework for registering our user code ctors & dtors. I set this up to build as a library file.
- cdtors.bas
- cdtors.bi
$ fbc -lib -g cdtors.bas
And a demo program:
- main.bas
- f1.bas
- f2.bas
$ fbc main.bas f1.bas f2.bas
Example OUTPUT:
f2.bas:CTOR
f1.bas:CTORA
f1.bas:CTORB
main.bas:CTOR
MAIN MODULE USER CODE
main.bas:DTOR
f1.bas:DTORB
f1.bas:DTORA
f2.bas:DTOR
cdtors.bi
API for the cdtors routines
cdtors.bas
main.bas
f1.bas
f2.bas
HOW IT WORKS:
1) RegisterUserCTOR( procptr( ctor ) ) and RegisterUserDTOR( procptr( dtor ) ) functions are called from the built-in module constructors (Global Ctors).
2) By registering a procedure, it's add to a singly linked list, either the CTOR list or the DTOR list
3) To avoid dealing with dynamic memory management in a Global Ctor, RegisterUserCTOR() and RegisterUserDTOR() are actually macros that allocate a static variable to hold the address of the procedure and a pointer to the next node in the list
4) Once execution gets to the user code, it's up the the user to call doUserCtors()
5) When doUserCtors() completes, it also adds an exit handler for doUserDtors() that will be called automatically when the program exits
6) Or user can call doUserDtors() directly for cleanup.
IDEAS for EXTENSIONS:
Now that the startup / exit code is under the control of the user, there could be a number of extensions could add to this:
- Ctors and Dtors with returning some value to indicate success or failure. This could allow presenting the user with more error information or logging diagnostics to a file as part of the framework itself.
- groupings or priorities for ctors and dtors to control the order or dependencies of initialization or cleanup. The example above just adds the procedures in the order that they are registered (reverse order for dtors).
- Passing parameter(s) to constructors to control initialization
- build the framework in to the compiler / run time keep and use linker / gcc / crt global ctors/dtors only for really low level stuff and not general user code
Here we're going to build our own user code for constructors / destructors. We will still use the built-in module constructors / destructors (aka global ctors & dtors) but only to register the initialization and exit procedures (aka user ctors and dtors) in our user code. And we delay the execution of our procedures until we have control in main module user code.
To make this work, we need a little framework for registering our user code ctors & dtors. I set this up to build as a library file.
- cdtors.bas
- cdtors.bi
$ fbc -lib -g cdtors.bas
And a demo program:
- main.bas
- f1.bas
- f2.bas
$ fbc main.bas f1.bas f2.bas
Example OUTPUT:
f2.bas:CTOR
f1.bas:CTORA
f1.bas:CTORB
main.bas:CTOR
MAIN MODULE USER CODE
main.bas:DTOR
f1.bas:DTORB
f1.bas:DTORA
f2.bas:DTOR
cdtors.bi
API for the cdtors routines
Code: Select all
#pragma once
#inclib "cdtors"
#ifndef NULL
#define NULL 0
#endif
namespace cdtors
type USERCTORDTORPROC as sub()
type USERCTORDTORNODE
proc as USERCTORDTORPROC '' pointer to the ctor/dtor sub
next as USERCTORDTORNODE ptr '' pointer to next node in the list
end type
declare sub registerUserCtorNode _
( _
byval node as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
)
declare sub registerUserDtorNode _
( _
byval node as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
)
declare sub doUserCtors()
declare sub doUserDtors()
end namespace
#macro RegisterUserCTOR( procptr )
__FB_UNIQUEID_PUSH__( usercdtors )
static __FB_UNIQUEID__( usercdtors ) as cdtors.USERCTORDTORNODE
cdtors.registerUserCtorNode( @__FB_UNIQUEID__( usercdtors ), procptr )
__FB_UNIQUEID_POP__( usercdtors )
#endmacro
#macro RegisterUserDTOR( procptr )
__FB_UNIQUEID_PUSH__( usercdtors )
static __FB_UNIQUEID__( usercdtors ) as cdtors.USERCTORDTORNODE
cdtors.registerUserDtorNode( @__FB_UNIQUEID__( usercdtors ), procptr )
__FB_UNIQUEID_POP__( usercdtors )
#endmacro
Code: Select all
#include once "cdtors.bi"
namespace cdtors
'' singly-linked list of constructors and destructors
dim shared ctorhead as USERCTORDTORNODE ptr = 0 '' NULL
dim shared dtorhead as USERCTORDTORNODE ptr = 0 '' NULL
private function findUserNodeByProc _
( _
byval head as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
) as USERCTORDTORNODE ptr
while( head )
if( head->proc = proc ) then
return head
end if
head = head->next
wend
return NULL
end function
private sub linkUserNodeFirst _
( _
byref head as USERCTORDTORNODE ptr, _
byval node as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
)
'' initialize the node and add it to the head
node->proc = proc
node->next = head
head = node
end sub
private sub linkUserNodeLast _
( _
byref head as USERCTORDTORNODE ptr, _
byval node as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
)
if( head = NULL ) then
linkUserNodeFirst( head, node, proc )
return
end if
'' find the tail
var tail = head
while( tail->next )
tail = tail->next
wend
'' initialize the node and add it to the tail
node->proc = proc
node->next = NULL
tail->next = node
end sub
private function canInitUserNode _
( _
byval head as USERCTORDTORNODE ptr, _
byval node as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
) as USERCTORDTORNODE ptr
'' can't add a node if it's NULL
if( node = NULL ) then
return NULL
end if
'' don't add a proc if it's NULL
if( proc = NULL ) then
return NULL
end if
'' don't add a proc if we already have it
if( findUserNodeByProc( head, proc ) ) then
return NULL
end if
return node
end function
public sub registerUserCtorNode _
( _
byval node as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
)
if( canInitUserNode( ctorhead, node, proc ) ) then
linkUserNodeLast( ctorhead, node, proc )
end if
end sub
public sub registerUserDtorNode _
( _
byval node as USERCTORDTORNODE ptr, _
byval proc as USERCTORDTORPROC _
)
if( canInitUserNode( dtorhead, node, proc ) ) then
linkUserNodeFirst( dtorhead, node, proc )
end if
end sub
private sub callCtorDtorList( byval node as USERCTORDTORNODE ptr )
while( node andalso node->proc )
node->proc()
node = node->next
wend
end sub
public sub doUserDtors()
'' only allow calling doUserDtors() once
static called_already as boolean = false
if( called_already ) then
exit sub
end if
called_already = true
callCtorDtorList( dtorhead )
end sub
private sub doUserDtorsAtExit cdecl ()
doUserDtors()
end sub
public sub doUserCtors()
'' only allow calling doUserCtors() once
static called_already as boolean = false
if( called_already ) then
exit sub
end if
called_already = true
callCtorDtorList( ctorhead )
'' fb_atexit( byval handler as sub cdecl () )
'' is an undocumented built-in. It's used to
'' call destructors for static variables when
'' the program ends
fb_atexit( procptr( doUserDtorsAtExit ) )
end sub
end namespace
Code: Select all
#include once "cdtors.bi"
private sub ctor()
print __FILE__ & ":" & __FUNCTION__
end sub
private sub dtor()
print __FILE__ & ":" & __FUNCTION__
end sub
'' start of MAIN MODULE user code
'' we can register CTORS/DTORS from user code
'' before we call cdtors.doUserCtors()
RegisterUserCTOR( procptr( ctor ) )
RegisterUserDTOR( procptr( dtor ) )
'' do all the user constructors
cdtors.doUserCtors()
scope
print "MAIN MODULE USER CODE"
end scope
'' exit by calling all the dtors
cdtors.doUserDtors()
Code: Select all
#include once "cdtors.bi"
private sub ctorA()
print __FILE__ & ":" & __FUNCTION__
end sub
private sub ctorB()
print __FILE__ & ":" & __FUNCTION__
end sub
private sub dtorA()
print __FILE__ & ":" & __FUNCTION__
end sub
private sub dtorB()
print __FILE__ & ":" & __FUNCTION__
end sub
RegisterUserCTOR( procptr( ctorA ) )
RegisterUserDTOR( procptr( dtorA ) )
RegisterUserCTOR( procptr( ctorB ) )
RegisterUserDTOR( procptr( dtorB ) )
Code: Select all
#include once "cdtors.bi"
private sub ctor()
print __FILE__ & ":" & __FUNCTION__
end sub
private sub dtor()
print __FILE__ & ":" & __FUNCTION__
end sub
RegisterUserCTOR( procptr( ctor ) )
RegisterUserDTOR( procptr( dtor ) )
HOW IT WORKS:
1) RegisterUserCTOR( procptr( ctor ) ) and RegisterUserDTOR( procptr( dtor ) ) functions are called from the built-in module constructors (Global Ctors).
2) By registering a procedure, it's add to a singly linked list, either the CTOR list or the DTOR list
3) To avoid dealing with dynamic memory management in a Global Ctor, RegisterUserCTOR() and RegisterUserDTOR() are actually macros that allocate a static variable to hold the address of the procedure and a pointer to the next node in the list
4) Once execution gets to the user code, it's up the the user to call doUserCtors()
5) When doUserCtors() completes, it also adds an exit handler for doUserDtors() that will be called automatically when the program exits
6) Or user can call doUserDtors() directly for cleanup.
IDEAS for EXTENSIONS:
Now that the startup / exit code is under the control of the user, there could be a number of extensions could add to this:
- Ctors and Dtors with returning some value to indicate success or failure. This could allow presenting the user with more error information or logging diagnostics to a file as part of the framework itself.
- groupings or priorities for ctors and dtors to control the order or dependencies of initialization or cleanup. The example above just adds the procedures in the order that they are registered (reverse order for dtors).
- Passing parameter(s) to constructors to control initialization
- build the framework in to the compiler / run time keep and use linker / gcc / crt global ctors/dtors only for really low level stuff and not general user code
Re: Executables and Compiling
I will perhaps soon (if no one does before) devote myself to completing the 'Executables' article in the 'Programmer's Guide' ('Making Binaries' section), from the above Jeff's posts and by synthesizing there only the information most useful to a programmer.
-
- Posts: 606
- Joined: Nov 28, 2012 1:27
- Location: CA, USA moving to WA, USA
- Contact:
Re: Executables and Compiling
I agree that a user (of BASIC) should not need to be concerned with where/what/how a compiled program starts after the language init code.
@coderjeff:
Perhaps before there is an effort made to make changes, maybe we could learn what problem, if any, is perceived to be with the current system of CTORs and DTORs? Or is this discussion just for the purposes of good documentation?
Any sophisticated user already knows how to use the non class/type 'main module' CTORs and DTORs, and probably has for some time. I'm wondering what "fixes" would benefit a beginner and not break other older mature code. Too loud a warning on the keyword pages and beginners might not test them. That would be a shame.
Certainly, a better explanation of existing destructors would be helpful to me, especially relating to the graphics. These are the most usual hangups for me, though the threading fixes corrected most of the problems.
Maybe a 'fix' to the END command would be more appropriate? Currently, it has no place in a 'finished' program and isn't even worth using for debugging.
I also think that maybe you are making the topic too complex and confusing. Any code that a user writes and is not attaching itself to FB internals to be run before his own modules is user code, however it is included or constructed. From the LANGUAGE viewpoint: if code is processed with '-pp' and is visible in that listing, it is user code and would be considered part of the user 'main' module, though maybe not the formal '_main_' in the final executable. (What is what?) The only possible exceptions would be CTORs and DTORs defined as such by the user.
Describing where they exist in the FB execution chain would be of benefit. Default CTORS and DTORS for classes and types and where they in the FB startup sequencing might help, also. It is ASSUMED they happen after ALL the FB init code/before the FB closing code. Is that the case?
I support the good, in-depth explanations being generated lately, if for no other reason than to clarify everyone's thoughts about how things actually work.
Whatever is written, the viewpoint of the terminology must be clear: user or language processor.
david
@coderjeff:
I don't see it, but I hear an implicit 'something needs to be changed' thought floating around that statement. I'm sure I am wrong?For these module constructors and module destructors, it really should be noted with a big WARNING on their use. Because this code is run outside of user code, it's quite likely that fbc's error checking and some runtime facilities won't work as expected.
Perhaps before there is an effort made to make changes, maybe we could learn what problem, if any, is perceived to be with the current system of CTORs and DTORs? Or is this discussion just for the purposes of good documentation?
Any sophisticated user already knows how to use the non class/type 'main module' CTORs and DTORs, and probably has for some time. I'm wondering what "fixes" would benefit a beginner and not break other older mature code. Too loud a warning on the keyword pages and beginners might not test them. That would be a shame.
Certainly, a better explanation of existing destructors would be helpful to me, especially relating to the graphics. These are the most usual hangups for me, though the threading fixes corrected most of the problems.
Maybe a 'fix' to the END command would be more appropriate? Currently, it has no place in a 'finished' program and isn't even worth using for debugging.
I also think that maybe you are making the topic too complex and confusing. Any code that a user writes and is not attaching itself to FB internals to be run before his own modules is user code, however it is included or constructed. From the LANGUAGE viewpoint: if code is processed with '-pp' and is visible in that listing, it is user code and would be considered part of the user 'main' module, though maybe not the formal '_main_' in the final executable. (What is what?) The only possible exceptions would be CTORs and DTORs defined as such by the user.
Describing where they exist in the FB execution chain would be of benefit. Default CTORS and DTORS for classes and types and where they in the FB startup sequencing might help, also. It is ASSUMED they happen after ALL the FB init code/before the FB closing code. Is that the case?
I support the good, in-depth explanations being generated lately, if for no other reason than to clarify everyone's thoughts about how things actually work.
Whatever is written, the viewpoint of the terminology must be clear: user or language processor.
david
Re: Executables and Compiling
A picture perhaps? For the wiki docs I mean?
Extensions are unix convention vs windows (ld does .lib too nowadays). In the case of shared libs, the name for Mac deviates (.dylib)
Extensions are unix convention vs windows (ld does .lib too nowadays). In the case of shared libs, the name for Mac deviates (.dylib)
Re: Executables and Compiling
ProPgExecutables → fxm [page complemented]fxm wrote:I will perhaps soon (if no one does before) devote myself to completing the 'Executables' article in the 'Programmer's Guide' ('Making Binaries' section), from the above Jeff's posts and by synthesizing there only the information most useful to a programmer.