A1: 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.
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 - You may not use dynamic memory allocation (e.g.
mallocandfree). - You may add as many helper functions as you would like in
my_printf.c(including those you wrote in Lab 1!), but you must leave the function signatures formy_printfandprint_integerunchanged. You may not changemy_printf.h, as we will be using our own header file for grading.
Provided Files
The provided release code contains six 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 already included in this file.my_printf.c, which contains the function definitions for your implementation. This is where you will write your code formy_printfandprint_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.smoke_test.candsmoke_test.txt, which are the smoke test we run on your submission and its expected output.
Getting Started
To get started, obtain the release code by cloning the a1 repository from GitHub:
git clone git@github.coecis.cornell.edu:cs3410-2026fa-student/<YOUR NET ID>_a1.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-2026fa-student/zw669_a1.git
Overview
In this assignment you will implement your own version of printf (see the
documentation here) called
my_printf from scratch. 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 integerntostdoutin the specified base (radix), withprefiximmediately 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 1: You will implement the
print_integerfunction - Task 2: You will implement the
my_printffunction
Implementation
Task 1: print_integer
Note
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.candtest_my_printf.c.If you would like to use the
print_digitandprint_stringfunctions that you wrote in Lab 1, you should copy and paste them intomy_printf.cfrom yourlab1.cfile that you implemented for Lab 1.
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 canonically 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”print_integer(3410, 5, "")should print “102120”print_integer(-3410, 5, "")should print “32244002321021”
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.
Warning
You are not allowed to call any functions from the C standard library except for
fputcanywhere in your implementation. You should print a character to the console usingfputc(c, stdout), wherecis 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 fputcto pull it up. You can scroll through it and then type
qto exit.
On our platform, an int is 32 bits, and you may assume this in your solution. Dynamic memory allocation (malloc) is prohibited, as stdlib.h is not allowed, but you may use fixed-size local variables and arrays. Other string manipulation or streaming functions are also prohibited, such as strcpy, strncpy, strcat, strchr, strtok, sprintf, printf.
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", "Weatherspoon", "Susag")should print “Professor Weatherspoon and Professor Susag 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
Note
Like many commands on this page, this assumes you have the
rvaliases setup as described in our RISC-V Infrastructure setup guide.
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
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
- One or more tests with positive numbers
- One or more tests with negative numbers
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 smoke test to ensure your code compiles and passes the public test cases.
Run smoke Test locally
You can build smoke test locally with the following command:
rv gcc -Wall -Wextra -Wpedantic -Wshadow -std=c17 -o smoke_test smoke_test.c my_printf.c
To test this code, you must execute the resulting file and pipe your print results to a file, such as with the following command:
rv qemu smoke_test > smoke_out.txt
Finally, you must compare the resulting prints to our expected results using diff:
diff --strip-trailing-cr smoke_out.txt smoke_test.txt
If you observe any differences between the two, a printing test failed.
You can also combine these operations into a single bash command:
rv gcc -Wall -Wextra -Wpedantic -Wshadow -std=c17 -o smoke_test smoke_test.c my_printf.c && \
rv qemu smoke_test > smoke_out.txt && \
diff --strip-trailing-cr smoke_out.txt smoke_test.txt
Rubric
- 40 points:
print_integercorrectness - 50 points:
my_printfcorrectness - 10 points: test quality