473,549 Members | 2,588 Online
Bytes | Software Development & Data Engineering Community
+ Post

Home Posts Topics Members FAQ

A simple parser

Hi guys

I have written this small parser to print out the functions defined in a
C file. This is an example of parsing in C, that I want to add to my
tutorial. Comments (and bug reports) are welcome.

-------------------------------------------------------------cut here

/* A simple scanner that will take a file of C source code and
print the names of all functions therein, in the following format:
"Function XXXX found line dddd .... ddddd"
Algorithm. It scans for a terminating parentheses and an immediately
following opening brace. Comments can appear between the closing
paren and the opening braces, but no other characters besides white
space. Functions must have the correct prototype, K & R syntax
is not supported.
*/
#include <stdio.h>
#define MAXID 1024 // Longest Identifier we support. Sorry
// Java guys...
static char IdBuffer[MAXID]; // Buffer for remembering the function name
static int line = 1; // We start at line 1

// This function reads a character and if
// it is \n it bumps the line counter
static int Fgetc(FILE *f)
{
int c = fgetc(f);
if (c == '\n')
line++;
return c;
}

// Return 1 if the character is a legal C identifier
// character, zero if not. The parameter "start"
// means if an identifier START character
// (numbers) is desired.
static int IsIdentifier(in t c,int start)
{
if (c >= 'a' && c <= 'z')
return 1;
if (c >= 'A' && c <= 'Z')
return 1;
if (start == 0 && c >= '0' && c <= '9')
return 1;
if (c == '_')
return 1;
return 0;
}

// Just prints the function name
static int PrintFunction(F ILE *f)
{
printf("Functio n %s found line %d ...",IdBuffer,l ine);
return Fgetc(f);
}

// Reads a global identifier into our name buffer
static int ReadId(char c,FILE *f)
{
int i = 1;
IdBuffer[0] = c;
while (i < MAXID-1) {
c = Fgetc(f);
if (c != EOF) {
if (IsIdentifier(c ,0))
IdBuffer[i++] = c;
else break;
}
else break;
}
IdBuffer[i] = 0;
return c;
}
static int ParseString(FIL E *f) // Skips strings
{
int c = Fgetc(f);
while (c != EOF && c != '"') {
if (c == '\\')
c = Fgetc(f);
if (c != EOF)
c = Fgetc(f);
}
if (c == '"')
c = Fgetc(f);
return c;
}

static int ParseComment(FI LE *f) // Skips comments
{
int c = Fgetc(f);
restart:
while (c != '*') {
c = Fgetc(f);
if (c == EOF)
return EOF;
}
c = Fgetc(f);
if (c == '/')
return Fgetc(f);
else goto restart;
}
static int ParseCppComment (FILE *f) // Skips // comments
{
int c = Fgetc(f);
while (c != EOF && c != '\n') {
if (c == '\\')
c = Fgetc(f);
if (c != EOF)
c = Fgetc(f);
}
if (c == '\n')
c = Fgetc(f);
return c;
}

// Skips white space and comments
static int SkipWhiteSpace( int c,FILE *f) {
if (c ' ')
return c;
while (c <= ' ') {
c = Fgetc(f);
if (c == '/') {
c = Fgetc(f);
if (c == '*')
c = ParseComment(f) ;
else if (c == '/')
c = ParseCppComment (f);
}
}
return c;
}

// Skips chars between simple quotes
static int ParseQuotedChar (FILE *f)
{
int c = Fgetc(f);
while (c != EOF && c != '\'') {
if (c == '\\')
c = Fgetc(f);
if (c != EOF)
c = Fgetc(f);
}
if (c == '\'')
c = Fgetc(f);
return c;
}
int main(int argc,char *argv[])
{
if (argc == 1) {
printf("Usage: %s <file.c>\n",arg v[0]);
return 1;
}
FILE *f = fopen(argv[1],"r");
if (f == NULL) {
printf("Can't find %s\n",argv[1]);
return 2;
}
int c = Fgetc(f);
int level = 0;
int parenlevel = 0;
int inFunction = 0;
while (c != EOF) {
// Note that each of the switches must advance the
// character read so that we avoid an infinite loop.
switch (c) {
case '"':
c = ParseString(f);
break;
case '/':
c = Fgetc(f);
if (c == '*')
c = ParseComment(f) ;
else if (c == '/')
c = ParseCppComment (f);
break;
case '\'':
c = ParseQuotedChar (f);
break;
case '{':
level++;
c = Fgetc(f);
break;
case '}':
if (level == 1 && inFunction) {
printf(" %d\n",line);
inFunction = 0;
}
if (level 0)
level--;
c = Fgetc(f);
break;
case '(':
parenlevel++;
c = Fgetc(f);
break;
case ')':
if (parenlevel 0)
parenlevel--;
c = Fgetc(f);
if ((parenlevel|le vel) == 0) {
c = SkipWhiteSpace( c,f);
if (c == '{') {
level++;
inFunction = 1;
c = PrintFunction(f );
}
}
break;
default:
if ((level | parenlevel) == 0 &&
IsIdentifier(c, 1))
c = ReadId(c,f);
else c = Fgetc(f);
}
}
fclose(f);
return 0;
}
Oct 14 '06 #1
121 6428

jacob navia wrote:
>It scans for a terminating parentheses and an immediately
following opening brace.
Um, can't if() and while() statements have that sequence?
A better strategy might be to count curly brackets. If the nesting
level of curlys is zero, then you're at the top level. Anything that
has parens is probably a function declaration.

Of course there's the problem of macros,which I'm sure you don't want
to handle yourself. Maybe run cpp and pipe its output to your program?

Oct 15 '06 #2
jacob navia wrote:
>
I have written this small parser to print out the functions
defined in a C file. This is an example of parsing in C, that I
want to add to my tutorial. Comments (and bug reports) are welcome.
.... snip ...
>
// Return 1 if the character is a legal C identifier
// character, zero if not. The parameter "start"
// means if an identifier START character
// (numbers) is desired.
static int IsIdentifier(in t c,int start)
{
if (c >= 'a' && c <= 'z')
return 1;
if (c >= 'A' && c <= 'Z')
return 1;
if (start == 0 && c >= '0' && c <= '9')
return 1;
if (c == '_')
return 1;
return 0;
}
Just this one example will do. Obvious problems:

1. Excessive indentation. Later it makes lines much too long.
Use spaces, not tabs, and indent by 3 or 4 places.

2. Use of // comments in Usenet. Causes nasty line wrapping
problems.

3. Faulty code. There is no guarantee 'a' .. 'z' etc. are
contiguous, in any specific order, etc.

4. Doesn't handle EOF.

Here is a sample of some code that may do what you want. It
doesn't allow line continuation within identifiers. You would
probably need to control '{' and '}' matching within skiptoident.

/* skips and echoes all characters that cannot start an
identifier. echoto may be NULL, to suppress any echoing
Returns the starting char for the following identifier,
which may be EOF and is still in the input stream.
*/
static int skiptoident(FIL E *f, FILE *echoto)
{
int ch;

do {
ch = getc(f);
if (('_' == ch) || isalpha(ch)) break;
if (echoto && (EOF != ch)) putc(ch, echoto);
} while (EOF != ch);
ungetc(ch, f);
return ch;
} /* skiptoident */

/* ----------------- */

/* generic function to extract the next identifier from a stream
Identifiers can begin with _ or alpha, and continue until a
non-alpha non _ non-numeric char is encountered.
'\0' terminate the string.
Leading chars that do not fit an identifier are echoed.
echoto may be NULL, to suppress any echoing
Returns char terminating the identifier, which is 'ungotten'
buf[bmax] should be valid storage space.
*/
static int getident(char *buf, int bmax, FILE *f, FILE *echoto)
{
int ch, ix;

if (EOF == skiptoident(f, echoto)) ch = EOF;
else ch = getc(f);
/* Here the input stream has been absorbed and echoed */
/* up to the first character of an identifier, in ch */
ix = 0;
while (('_' == ch) || isalnum(ch)) { /* skips on eof */
buf[ix++] = ch;
ch = getc(f);
if (ix >= bmax) break;
}
buf[ix] = '\0';
ungetc(ch, f);
return ch;
} /* getident */

These are extracts from id2id which has to do some elementary C
lexing, and is available on my site:

<http://cbfalconer.home.att.net/download/

PS: I find you have committed the sin of multi-posting, so I have
cross-posted this response. Luckily I didn't go back on line
before checking the lcc group.

--
"I don't know where bin Laden is. I have no idea and really
don't care. It's not that important." - G.W. Bush, 2002-03-13
"No, we've had no evidence that Saddam Hussein was involved
with September the 11th." - George Walker Bush 2003-09-17
Oct 15 '06 #3
jacob navia said:
Hi guys

I have written this small parser to print out the functions defined in a
C file. This is an example of parsing in C, that I want to add to my
tutorial. Comments (and bug reports) are welcome.
foo.c:12: parse error before `/'
foo.c:17: stray '\' in program
foo.c:54: warning: type defaults to `int' in declaration of `IdBuffer'
foo.c:54: warning: ANSI C forbids zero-size array `IdBuffer'
foo.c:54: `c' undeclared here (not in a function)
foo.c:54: ANSI C forbids data definition with no type or storage class
foo.c:55: parse error before `while'
foo.c:64: `i' undeclared here (not in a function)
foo.c:64: warning: type defaults to `int' in declaration of `IdBuffer'
foo.c:64: variable `IdBuffer' has initializer but incomplete type
foo.c:64: conflicting types for `IdBuffer'
foo.c:54: previous declaration of `IdBuffer'
foo.c:64: ANSI C forbids data definition with no type or storage class
foo.c:65: parse error before `return'
foo.c:69: parse error before `/'
foo.c:83: parse error before `/'
foo.c:92: warning: type defaults to `int' in declaration of `c'
foo.c:92: warning: implicit declaration of function `Fgetc'
foo.c:92: `f' undeclared here (not in a function)
foo.c:92: initializer element is not constant
foo.c:92: ANSI C forbids data definition with no type or storage class
foo.c:93: parse error before `if'
foo.c:99: parse error before `/'
foo.c: In function `main':
foo.c:152: parse error before `*'
foo.c:153: `f' undeclared (first use in this function)
foo.c:153: (Each undeclared identifier is reported only once
foo.c:153: for each function it appears in.)
foo.c:157: parse error before `int'
foo.c:162: parse error before `/'
foo.c:168: case label not within a switch statement
foo.c:171: warning: implicit declaration of function `ParseComment'
foo.c:173: warning: implicit declaration of function `ParseCppCommen t'
foo.c:175: case label not within a switch statement
foo.c:176: warning: implicit declaration of function `ParseQuotedCha r'
foo.c:178: case label not within a switch statement
foo.c:179: `level' undeclared (first use in this function)
foo.c:182: case label not within a switch statement
foo.c:183: `inFunction' undeclared (first use in this function)
foo.c:184: `line' undeclared (first use in this function)
foo.c:191: case label not within a switch statement
foo.c:192: `parenlevel' undeclared (first use in this function)
foo.c:195: case label not within a switch statement
foo.c:200: warning: implicit declaration of function `SkipWhiteSpace '
foo.c:204: warning: implicit declaration of function `PrintFunction'
foo.c:208: default label not within a switch statement
foo.c:210: warning: implicit declaration of function `IsIdentifier'
foo.c:211: warning: implicit declaration of function `ReadId'
foo.c: At top level:
foo.c:215: warning: type defaults to `int' in declaration of `fclose'
foo.c:215: warning: parameter names (without types) in function declaration
foo.c:215: ANSI C forbids data definition with no type or storage class
foo.c:216: parse error before `return'
make: *** [foo.o] Error 1

--
Richard Heathfield
"Usenet is a strange place" - dmr 29/7/1999
http://www.cpax.org.uk
email: rjh at above domain (but drop the www, obviously)
Oct 15 '06 #4
Richard Heathfield <in*****@invali d.invalidwrites :
jacob navia said:
>Hi guys

I have written this small parser to print out the functions defined in a
C file. This is an example of parsing in C, that I want to add to my
tutorial. Comments (and bug reports) are welcome.

foo.c:12: parse error before `/'
foo.c:17: stray '\' in program
[51 lines deleted]
make: *** [foo.o] Error 1
Every one of those errors is caused by two things: "//" comments and
mixed declarations and statements.

The code compiles without error with "gcc -std=c99". It also compiles
without error with "gcc -ansi -pedantic" if I remove the "//" comments
and move a few object declarations.

It is, as far as I can tell, valid C99.

I understand and agree with your reasons for using a strict C90
compiler, but not everyone does so.

--
Keith Thompson (The_Other_Keit h) ks***@mib.org <http://www.ghoti.net/~kst>
San Diego Supercomputer Center <* <http://users.sdsc.edu/~kst>
We must do something. This is something. Therefore, we must do this.
Oct 15 '06 #5
Keith Thompson said:
Richard Heathfield <in*****@invali d.invalidwrites :
>jacob navia said:
>>Hi guys

I have written this small parser to print out the functions defined in a
C file. This is an example of parsing in C, that I want to add to my
tutorial. Comments (and bug reports) are welcome.

foo.c:12: parse error before `/'
foo.c:17: stray '\' in program
[51 lines deleted]
>make: *** [foo.o] Error 1

Every one of those errors is caused by two things: "//" comments and
mixed declarations and statements.
<shrugI figured it had to be something like that. So - does anyone have a
conforming C99 compiler that we can use to test Mr Navia's code? No? Oh
well.
The code compiles without error with "gcc -std=c99".
Er, so what? Despite the misleading switch-name, gcc is not a C99-conforming
compiler.
It also compiles
without error with "gcc -ansi -pedantic" if I remove the "//" comments
and move a few object declarations.
That, in my view, is the author's job, not yours or mine. (And it shows that
the C99isms have been adopted gratuitously, presumably to make the code
less portable. Why anyone should wish to do this is beyond me.)
It is, as far as I can tell, valid C99.

I understand and agree with your reasons for using a strict C90
compiler, but not everyone does so.
Well, my principal reason for using a strict C90 compiler is simple - it's
so that I can be sure that /my/ code doesn't use any C99isms, because I
want /my/ code to compile everywhere, not just on the vanishingly small
number of conforming C99 implementations .

--
Richard Heathfield
"Usenet is a strange place" - dmr 29/7/1999
http://www.cpax.org.uk
email: rjh at above domain (but drop the www, obviously)
Oct 15 '06 #6
Ancient_Hacker wrote:
jacob navia wrote:
>>It scans for a terminating parentheses and an immediately
following opening brace.


Um, can't if() and while() statements have that sequence?
A better strategy might be to count curly brackets. If the nesting
level of curlys is zero, then you're at the top level. Anything that
has parens is probably a function declaration.
Well I *do* count parens and brackets.... See the code
>
Of course there's the problem of macros,which I'm sure you don't want
to handle yourself. Maybe run cpp and pipe its output to your program?
Macros will be assumed function calls, but not function definitions...

I should ignore # lines, that would make it more robust.
Oct 15 '06 #7
CBFalconer wrote:
jacob navia wrote:
>>I have written this small parser to print out the functions
defined in a C file. This is an example of parsing in C, that I
want to add to my tutorial. Comments (and bug reports) are welcome.

... snip ...
>>// Return 1 if the character is a legal C identifier
// character, zero if not. The parameter "start"
// means if an identifier START character
// (numbers) is desired.
static int IsIdentifier(in t c,int start)
{
if (c >= 'a' && c <= 'z')
return 1;
if (c >= 'A' && c <= 'Z')
return 1;
if (start == 0 && c >= '0' && c <= '9')
return 1;
if (c == '_')
return 1;
return 0;
}


Just this one example will do. Obvious problems:

1. Excessive indentation. Later it makes lines much too long.
Use spaces, not tabs, and indent by 3 or 4 places.
OK
2. Use of // comments in Usenet. Causes nasty line wrapping
problems.
OK
3. Faulty code. There is no guarantee 'a' .. 'z' etc. are
contiguous, in any specific order, etc.
Mmmm, it *could* be, but I have never found a machine where they aren't
contiguous...
4. Doesn't handle EOF.
Why? It returns 0 at EOF, and EOF is NOT an identifier char, so it is
correct...
Here is a sample of some code that may do what you want. It
doesn't allow line continuation within identifiers. You would
probably need to control '{' and '}' matching within skiptoident.

/* skips and echoes all characters that cannot start an
identifier. echoto may be NULL, to suppress any echoing
Returns the starting char for the following identifier,
which may be EOF and is still in the input stream.
*/
static int skiptoident(FIL E *f, FILE *echoto)
{
int ch;

do {
ch = getc(f);
if (('_' == ch) || isalpha(ch)) break;
if (echoto && (EOF != ch)) putc(ch, echoto);
} while (EOF != ch);
ungetc(ch, f);
return ch;
} /* skiptoident */
This is interesting since it would eliminate the need for a
buffer. The problem is that

typedef struct _tagFoo {
int a;
} FOO;

would print the _tagfoo...

I can't decide whether an indentifier is really a function until
I see a ')' followed by {, so I can't put it out immediately.

But I see the point of '_' || isalpha(c)

It is much better. Thanks.

/* ----------------- */

/* generic function to extract the next identifier from a stream
Identifiers can begin with _ or alpha, and continue until a
non-alpha non _ non-numeric char is encountered.
'\0' terminate the string.
Leading chars that do not fit an identifier are echoed.
echoto may be NULL, to suppress any echoing
Returns char terminating the identifier, which is 'ungotten'
buf[bmax] should be valid storage space.
*/
static int getident(char *buf, int bmax, FILE *f, FILE *echoto)
{
int ch, ix;

if (EOF == skiptoident(f, echoto)) ch = EOF;
else ch = getc(f);
/* Here the input stream has been absorbed and echoed */
/* up to the first character of an identifier, in ch */
ix = 0;
while (('_' == ch) || isalnum(ch)) { /* skips on eof */
buf[ix++] = ch;
ch = getc(f);
if (ix >= bmax) break;
}
buf[ix] = '\0';
ungetc(ch, f);
return ch;
} /* getident */

These are extracts from id2id which has to do some elementary C
lexing, and is available on my site:

<http://cbfalconer.home.att.net/download/

PS: I find you have committed the sin of multi-posting, so I have
cross-posted this response. Luckily I didn't go back on line
before checking the lcc group.
Oct 15 '06 #8
Keith Thompson wrote:
Richard Heathfield <in*****@invali d.invalidwrites :
>>jacob navia said:

>>>Hi guys

I have written this small parser to print out the functions defined in a
C file. This is an example of parsing in C, that I want to add to my
tutorial. Comments (and bug reports) are welcome.

foo.c:12: parse error before `/'
foo.c:17: stray '\' in program

[51 lines deleted]
>>make: *** [foo.o] Error 1


Every one of those errors is caused by two things: "//" comments and
mixed declarations and statements.

The code compiles without error with "gcc -std=c99". It also compiles
without error with "gcc -ansi -pedantic" if I remove the "//" comments
and move a few object declarations.

It is, as far as I can tell, valid C99.

I understand and agree with your reasons for using a strict C90
compiler, but not everyone does so.
Since you compiled Keith, can you pass it through some
C code... Does it work?

jacob
Oct 15 '06 #9
jacob navia said:
CBFalconer wrote:
<snip>
>3. Faulty code. There is no guarantee 'a' .. 'z' etc. are
contiguous, in any specific order, etc.

Mmmm, it *could* be, but I have never found a machine where they aren't
contiguous...
That doesn't mean such machines don't exist. I've spent several years
working on such machines. Look up "EBCDIC" in Google.

<snip>

--
Richard Heathfield
"Usenet is a strange place" - dmr 29/7/1999
http://www.cpax.org.uk
email: rjh at above domain (but drop the www, obviously)
Oct 15 '06 #10

This thread has been closed and replies have been disabled. Please start a new discussion.

Similar topics

3
2188
by: Kenneth Downs | last post by:
Well, I'm coming to the end of a large and exhausting project, done in my new favorite language PHP, and its time for a diversion. I'm wondering if anyone has experience with writing simple parsers. I've never done it myself, but I know they are not as mysterious as they may seem, it's a matter of finding the tools. The idea is to take...
13
2274
by: Paulo Pinto | last post by:
Hi, does anyone know of a Python package that is able to load XML like the XML::Simple Perl package does? For those that don't know it, this package maps the XML file to a dictionary.
4
2439
by: Leif K-Brooks | last post by:
I'm writing a site with mod_python which will have, among other things, forums. I want to allow users to use some HTML (<em>, <strong>, <p>, etc.) on the forums, but I don't want to allow bad elements and attributes (onclick, <script>, etc.). I would also like to do basic validation (no overlapping elements like <strong><em>foo</em></strong>,...
8
6486
by: Dan | last post by:
Using XML::Simple in perl is extreemly slow to parse big XML files (can be up to 250M, taking ~1h). How can I increase my performance / reduce my memory usage? Is SAX the way forward?
4
11437
by: Greg B | last post by:
Well since getopt() doesn't seem to be compatible with Windows, and the free implementation of it for Windows that I found still had some annoying restrictions, I thought I'd whip up a simple parser myself. Just wanted to see if anyone could provide me with some constructive criticism :) any feedback would be greatly appreciated...
1
3065
by: steve smith | last post by:
Hi I have just downloaded the Borland C# Builder and the Micorsoft ..Net framework SDK v1.1 from the borland webist, and i am trying to get a simple program to run, however I keep getting errors, any ideas why this might be happening? Program i am running is: namespace ExamProblem { using System;
26
495
by: jacob navia | last post by:
Summary: I have changed (as proposed by Chuck) the code to use isalpha() instead of (c>='a' && c <= 'z') etc. I agree that EBCDIC exists :-) I eliminated the goto statement, obviously it is better in a tutorial to stick to structured programming whenever possible...
4
2741
by: =?Utf-8?B?SmFu?= | last post by:
In my application the user can configure automation-scripts by inserting different "actions" into a "procedure". These different procedure- and action-objects are all translated into C# code before execution. One "action" type is an expression-evaluator. At the moment the expression the user writes into the action is just inserted into the...
11
1330
by: Stef Mientki | last post by:
hello, I need to translate the following string a = '(0, 0, 0, 255), (192, 192, 192, 255), True, 8' into the following list or tuple b = Is there a simple way to to this. (Not needed now, but might need it in the future: even deeper nested
7
1188
by: bvdp | last post by:
Is there a simple/safe expression evaluator I can use in a python program. I just want to pass along a string in the form "1 + 44 / 3" or perhaps "1 + (-4.3*5)" and get a numeric result. I can do this with eval() but I really don't want to subject my users to the problems with that method. In this use I don't need python to worry about...
0
7532
marktang
by: marktang | last post by:
ONU (Optical Network Unit) is one of the key components for providing high-speed Internet services. Its primary function is to act as an endpoint device located at the user's premises. However, people are often confused as to whether an ONU can Work As a Router. In this blog post, we’ll explore What is ONU, What Is Router, ONU & Router’s main...
0
7461
by: Hystou | last post by:
Most computers default to English, but sometimes we require a different language, especially when relocating. Forgot to request a specific language before your computer shipped? No problem! You can effortlessly switch the default language on Windows 10 without reinstalling. I'll walk you through it. First, let's disable language...
0
7730
Oralloy
by: Oralloy | last post by:
Hello folks, I am unable to find appropriate documentation on the type promotion of bit-fields when using the generalised comparison operator "<=>". The problem is that using the GNU compilers, it seems that the internal comparison operator "<=>" tries to promote arguments from unsigned to signed. This is as boiled down as I can make it. ...
0
7971
jinu1996
by: jinu1996 | last post by:
In today's digital age, having a compelling online presence is paramount for businesses aiming to thrive in a competitive landscape. At the heart of this digital strategy lies an intricately woven tapestry of website design and digital marketing. It's not merely about having a website; it's about crafting an immersive digital experience that...
0
7823
tracyyun
by: tracyyun | last post by:
Dear forum friends, With the development of smart home technology, a variety of wireless communication protocols have appeared on the market, such as Zigbee, Z-Wave, Wi-Fi, Bluetooth, etc. Each protocol has its own unique characteristics and advantages, but as a user who is planning to build a smart home system, I am a bit confused by the...
0
6055
agi2029
by: agi2029 | last post by:
Let's talk about the concept of autonomous AI software engineers and no-code agents. These AIs are designed to manage the entire lifecycle of a software development project—planning, coding, testing, and deployment—without human intervention. Imagine an AI that can take a project description, break it down, write the code, debug it, and then...
0
3491
by: adsilva | last post by:
A Windows Forms form does not have the event Unload, like VB6. What one acts like?
1
1068
muto222
by: muto222 | last post by:
How can i add a mobile payment intergratation into php mysql website.
0
776
bsmnconsultancy
by: bsmnconsultancy | last post by:
In today's digital era, a well-designed website is crucial for businesses looking to succeed. Whether you're a small business owner or a large corporation in Toronto, having a strong online presence can significantly impact your brand's success. BSMN Consultancy, a leader in Website Development in Toronto offers valuable insights into creating...

By using Bytes.com and it's services, you agree to our Privacy Policy and Terms of Use.

To disable or enable advertisements and analytics tracking please visit the manage ads & tracking page.