Implementing printf
Instructions:
Remember, all assignments in CS 3410 are individual.
You must submit work that is 100% your own.
Remember to ask for help from the CS 3410 staff in office hours or on Ed!
If you discuss the assignment with anyone else, be careful not to share your actual work, and include an acknowledgment of the discussion in a collaboration.txt
file along with your submission.
The assignment is due via Gradescope at 11:59pm on the due date indicated on the schedule.
Submission Requirements
You will submit your completed solution to this assignment to Gradescope. You must submit:
my_printf.c
, which will be modified with your solution for Task 1 and Task 2test_my_printf.c
, which will contain your tests for your solution for Task 1 and Task 2
Restrictions
- You may not include any libraries beyond what is already included in
my_printf.h
- Your solution should use constant space (you should not use arrays, either dynamically or statically)
- You may add as many helper functions as you would like in
my_printf.c
, but you must leave the function signatures formy_printf
andprint_integer
unchanged. You may not changemy_printf.h
, as we will be using our own header file for grading.
Provided Files
The provided release code contains four files:
my_printf.h
, which is a header file that contains the required function definitions and some useful include statements. You may not modify this file. You may also not include any libraries in your implementation beyond what is included in already in this file.my_printf.c
, which contains the function definitions for your implementation. This is where you will write your code formy_printf
andprint_integer
.test_my_printf.c
, which is a test file with a couple test cases to get you started. You must add more tests to receive full credit for this assignment.test_my_printf.txt
, which is a text file that you can use to compare your outputs to by “diff
” testing. See more in Running and Testing.
Getting Started
To get started, obtain the release code by cloning the a1
repository from GitHub:
$ git clone git@github.coecis.cornell.edu:cs3410-2024fa/<YOUR NET ID>_printf.git
- Note: Please replace the
<YOUR_NET_ID>
with your netid. For example, if you netid iszw669
, then this clone statement would begit clone git@github.coecis.cornell.edu:cs3410-2024fa/zw669_printf.git
Overview
In this assignment you will implement your own version of printf
(see the
documentation here) called
my_printf
without relying on the C standard library. Recall that printf
works
by taking in a format string that contains various format codes, in addition
to a variable number of other arguments. The format codes specify how to “plug in”
the arguments into the format string, to get the final result. For example:
printf("I love %d!", 3410); // prints "I love 3410!"
printf("Hello, %s", "Alan"); // prints "Hello, Alan"
printf("Hello %s and %s!", "Alan", "Alonzo"); // prints "Hello Alan and Alonzo!"
You will implement two key functions:
print_integer(int n, int radix, char *prefix)
: Print the integern
tostdout
in the specified base (radix
), withprefix
immediately before the first digit.my_printf(char *format, ...)
: Print a format string with any format codes replaced by the respective additional arguments.
Your implementation will be contained in my_printf.c
. We’ve provided you with the
function signatures to get you started. You should look at my_printf.h
for detailed
function specifications.
Assignment Outline
- Task 0: You will complete Task 0 in lab. This task is meant to build familiarity with the course tools and C as well as get you started on Task 1
- Task 1: You will implement the
print_integer
function - Task 2: You will implement the
my_printf
function
Implementation
(Lab) Task 0: Intro to C and print_integer
helper functions
View the lab slides here.
Before coming to lab, make sure to go through the course setup materials for git and the RISC-V Infrastructure. The lab tasks will assume you have at least set up your Cornell GitHub credentials and have your favorite text editor, such as Visual Studio Code, ready to go.
Step 1: Compiling and running C programs
Course Docker Container
Follow these instructions to set up Docker and obtain CS 3410’s Docker container. To summarize, you will need to:
- Install Docker itself.
- Download the image with
docker pull ghcr.io/sampsyo/cs3410-infra
. - Consider setting up an
rv
alias to make the container easy to use.
If you don’t already have a favorite text editor, now would also be a good time to install VSCode.
C Programming
Next, follow these instructions for writing, compiling, and running your first C program.
When your program runs, show the result to a TA. Congratulations! You’re now a C programmer.
Git
Now, we’ll get some experience with git! If you haven’t already, be sure to follow our guide to setting up your credentials on GitHub so you have an SSH key in place.
Go to the to the Cornell GitHub website and create a repository called “lab1”. This repository can be public, but for assignments all of your repositories must be private.
Now, clone your repository from within the cs3410
directory you made earlier:
$ git clone git@github.coecis.cornell.edu:abc123/lab1.git
replacing abc123
with your actual NetID. If this doesn’t work, ask a TA for
assistance. There is probably something wrong with your GitHub configuration.
Before changing directories into the
repo, you should move your hi.c
file that you created during the Docker setup step into the lab1
folder and clean up the executables
we made earlier:
$ mv hi.c lab1
$ rm a.out
$ cd lab1
$ ls
If you haven’t created one yet, just do:
$ cd lab1
$ printf '#include <stdio.h>\nint main() { printf("hi!\\n"); }\n' > hi.c
You should see the file hi.c
in your repository. Enter:
$ git status
The following should appear (or something like it):
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
hi.c
Now, you should add the file hi.c
to stage it, make a commit, and then
push to the remote repository:
$ git add hi.c
$ git commit -m "Initial commit"
$ git push
This is commonly the GitHub workflow for a single person working on an assignment. You’ll make some changes, commit them, and push them, over and over until you finish the assignment.
To learn more about git, consider following our complete git tutorial!
Step 2: print_digit
and print_string
For this next task, you are going to write two helper functions to help you in Task 1 and Task 2 of this assignment:
print_digit(int digit)
: Given an integer, print it to the terminal (without usingprintf
)print_string(char* s)
: Given a string, print it to the terminal (without usingprintf
)
You do not need to submit lab1.c
for A1. If you would like to use the print_digit
and print_string
functions as part of your implementation, you should copy and paste them into my_printf.c
.
First, cd
into your lab1
repository. Then, make a file called lab1.c
, and
copy/paste the following code:
#include <stdio.h>
// LAB TASK: Implement print_digit
void print_digit(int digit) {
}
// LAB TASK: Implement print_string
void print_string(char* s) {
}
int main(int argc, char* argv[]) {
printf("print_digit test: \n"); // Not to use this in A1
for (int i = 0; i <= 16; ++i) {
print_digit(i);
fputc(' ', stdout);
}
printf("\nprint_string test: \n"); // Not to use this in A1
char* str = "Hello, 3410\n";
print_string(str);
return 0;
}
Hint: For print_digit
, you’ll want to use an ASCII table.
Save the file and exit the editor. Now is a good time to commit and push your changes to your repository. Once you’ve pushed, try to implement the functions print_digit
and print_string
. The TAs are available for help should you need it.
Once you’ve implemented the functions, you can run the program:
$ rv gcc -Wall -Wextra -Wpedantic -Wshadow -std=c17 -o test_lab1 lab1.c
$ rv qemu test_lab1
Remember, if you change lab1.c
between runs, you need to recompile the program.
That’s all for this lab!
Task 1: print_integer
For Task 1 and Task 2, all your code should be in the “a1” Git repository. See the Getting Started section for how to retrieve the starter code. Your implementation will be contained in my_printf.c
and test_my_printf.c
.
The print_integer
function takes a number, a target base, and a prefix string and prints the
number in the target base with the prefix string immediately before the first
digit to stdout
. radix
may be any integer between 2 and 16 inclusive. For
values of radix
above 10, use lowercase letters to represent the digits
following 9 (since bases higher than 10 cannonically use lowercase letters as
well).
This function should not print a newline. Here are some examples:
print_integer(3410, 10, "")
should print “3410”print_integer(-3410, 10, "")
should print “-3410”print_integer(-3410, 10, "$")
should print “-$3410”print_integer(3410, 16, "")
should print “d52”print_integer(3410, 16, "0x")
should print “0xd52”print_integer(-3410, 2, "0b")
should print “0b11111111111111111111001010101110”print_integer(-3410, 16, "0x")
should print “0xfffff2ae”
For the radix 10
, negative numbers should be printed with a negative sign (-
).
All other bases should use the 2’s complement representation from lecture. In
other words, it should not print a negative sign, and instead just print
an unsigned integer representing a 2’s complement number. This is exactly what
printf
from the standard library does when you pass in negative integers for
bases other than 10. You can try this on your own:
#include <stdio.h>
int main() {
printf("-10 in hex is: %x\n", -10);
printf("-10 in binary is: %b\n", -10); // Note: requires C23
}
The above code outputs:
-10 in hex is: fffffff6
-10 in binary is: 11111111111111111111111111110110
which is the 2’s complement representation of -10 in hex and binary, respectively.
You are not allowed to call any functions from the C standard library except for fputc
anywhere in your implementation.
You should print a character to the console using fputc(c, stdout)
, where c
is the character you want to print.
Tip: In addition to the documentation on cppreference.com, you can also find documentation for many standard library functions in C through the manual pages (“manpages”) in your terminal. Simply type:
$ man fputc
to pull it up. You can scroll through it and then type q
to exit.
You must not make any assumptions about the size of an integer on a
given platform. On our platform, an integer is 32 bits, but C allows int
to
be different sizes on different platforms. For example, on some architectures
int
is 64 bits. Thus, you cannot store the new representation of the integer as a string or in a buffer of any size, as this would make assumptions about how big an integer is on your platform. Calling malloc
is also prohibited
(by extension of the fact that stdlib.h
is prohibited). In other words, you
should figure out how to do this without using any additional memory.
Storing characters or integers in an array (dynamically or statically) will result in a significant deduction.
You’ll also need to figure out how to print the integer from left-to-right instead of right-to-left without using additional memory. One of the algorithms you might recall from class for changing the base of a number would give you the digits from right-to-left, so it can seem tempting to try to use this as a starting point. Be warned that this will not work, as any tricks such as “reversing” the output or storing the digits would violate the constraints of this assignment (i.e. no standard library usage and no storing values in an array). Instead, think of how you can work backwards from the methods you’ve learned in class.
Task 2: my_printf
This function prints format
with any format codes replaced by the respective
additional arguments, as specified below:
Your my_printf
function is required to support the following format codes:
%d
: integer (int
,short
, orchar
), expressed in decimal notation, with no prefix.%x
: integer (int
,short
, orchar
), expressed in hexadecimal notation with the prefix “0x”. Lowercase letters are used for digits beyond 9%b
: integer (int
,short
, orchar
), expressed in binary notation with the prefix “0b”.%s
: string (char*
)%c
: character (int
,short
, orchar
, between 0 and 127) expressed as its corresponding ASCII character%%
: a single percent sign (no parameter)
For each occurrence of any of the above codes, your program shall print one of
the arguments (after the format) to my_printf(...)
in the specified format.
Anything else in the format string should be expressed as is. For example, if
the format string included "%z"
, then "%z"
would be printed. Likewise, a lone
“%” at the end of the string would also be printed as is (note that this differs
slightly from the behavior of printf
).
Note that strings in C can be NULL
. If my_printf
is passed a null string
as an argument, it should not crash, but instead print (null)
to represent the
would-be string:
#include <stdio.h>
int main(int argc, char* argv[]) {
my_printf("Null string: %s", NULL); // Prints: "Null string: (null)"
}
Again, you are not allowed to call any C standard library functions. You
should print to stdout
only using fputc
(documentation for fputc
is here).
For any format codes relating to numbers, your program should handle any valid
int values between INT_MIN
and INT_MAX
, inclusive.
Note that my_printf
is a variadic function, meaning it takes in a variable
number of arguments. You don’t need to know this deeply, but you will
need to look up the syntax, and also understand how a program determines the
number of arguments.
A variadic function is any function that takes in an unknown number of optional
parameters. The optional parameters are represented by three dots (e.g. int foo(int n, ...)
).
The dots are a part of the C language. The optional arguments are accessed using
va_arg
from stdarg.h
. You must call va_start
at the start of your
variadic function before the first use of va_arg
. You must call va_end
once
at the end of your variadic function, after the last use of va_arg
. There is
no way to know from va_arg
how many optional arguments there are, so you
need to use some other information to determine how many times to call va_arg
.
In this case, it is the format string. Here’s an example from the GNU
documentation:
#include <stdarg.h>
#include <stdio.h>
int add_em_up(int count,...) {
va_list ap;
va_start (ap, count); /* Initialize the argument list. */
int sum = 0;
for (int i = 0; i < count; i++)
sum += va_arg (ap, int); /* Get the next argument value. */
va_end (ap); /* Clean up. */
return sum;
}
int main(int argc, char* argv[]) {
/* This call prints 16. */
printf("%d\n", add_em_up (3, 5, 5, 6));
/* This call prints 55. */
printf("%d\n", add_em_up (10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
return 0;
}
Here are some examples to help you understand the spec:
my_printf("3410")
should print “3410”my_printf("My favorite class is %d", 3410)
should print “My favorite class is 3410”my_printf("%d in hex is %x", 3410, 3410)
should print “3410 in hex is 0xd52”my_printf("The pass rate in 3410 is 100%%")
should print “The pass rate in 3410 is 100%”my_printf("Professor %s and Professor %s are the instructors ", "Sampson", "Guidi")
should print “Professor Sampson and Professor Guidi are the instructors”
Note that insufficient parameters could lead to undefined behavior (i.e. when the number of arguments is less than the number of format codes). You do not have to handle this case. Similarly, mismatched parameters (when the format code does not match the given argument’s type) can also lead to undefined behavior, but you do not need to handle this.
You are encouraged to use print_integer
in my_printf
.
Nonetheless, these functions will be tested independently.
Running and Testing
To compile your code, run:
rv gcc -Wall -Wextra -Wpedantic -Wshadow -std=c17 -o test_my_printf test_my_printf.c my_printf.c
Then, to run your code:
rv qemu test_my_printf
Like many commands on this page, this assumes you have the rv
aliases setup as described in our RISC-V Infrastructure setup guide.
We will be testing your code by comparing the output of your program to a test
file. You will extend the file test_my_printf.txt
with your own
test cases. You are required to write more tests, and the quality of the tests
will be graded. Feel free to use the examples in this handout as a starting
point.
To receive full credit for testing, you should have at least 10 test cases
each for print_integer
and my_printf
. Test cases should cover as many
paths through your code as possible. To receive full credit for testing for print_integer
,
you should have at least:
- One test representing integers for each base from 2-16
- One or more tests for different prefixes
- One or more tests with no prefixes
To receive full credit for testing my_printf
you should have at least:
- One test for each format code
- One test for no format codes
- One test that contains multiple format codes
To compare the output of your program with the test file, run:
rv qemu test_my_printf > out.txt && diff out.txt test_my_printf.txt
If you don’t see any output from this command, your tests are passing. Note,
for each test you add in test_my_printf.txt
, you must call the corresponding
function (either print_integer
or my_printf
) in test_my_printf.c
. You
should insert newlines between your test cases for readability. You may use
printf
in your test file, if you wish.
Don’t forget to recompile your code between different runs of your program.
Note, you can do this all in one command, like such:
rv gcc -Wall -Wextra -Wpedantic -Wshadow -std=c17 -o test_my_printf test_my_printf.c my_printf.c && \
rv qemu test_my_printf > out.txt && \
diff out.txt test_my_printf.txt
Submission
Submit my_printf.c
and test_my_printf.c
to Gradescope.
Upon submission, we will provide a sanity check to ensure your code compiles and passes the public test cases.
Rubric
- 40 points:
print_integer
correctness - 50 points:
my_printf
correctness - 10 points: test quality