An exercise in frustration

Or, what to do when you want to buy a printer.

Last year our trusty Lexmark Optra S 1855 (networked, extra paper tray and duplexer) got to the end of its cartridge, and we found to our annoyance that a replacement would set us back close to AUD450. Not keen to take that option, we decided to get a schmick multifunction inkjet.

It was unfortunate that I didn’t check www.linuxprinting.org first, because I bought the Canon Pixma MP620. Sure, it’s a nice printer with respectable photo and plain paper printing, good copying and scanning as well. BUT if you don’t want to run a WindowsXP/Vista/7 or Mac to be a print server, then your option is running linux and using Canon’s binary blob libraries linked in with their (imnsho) poorly written ps to Canon converter coupled with cups-bjnp, the reverse-engineered BubbleJet Network Protocol implementation.

I’m not opposed to running linux, not by any stretch. However, I really do prefer to run the OS that I actually help develop, as much as humanly possible.

With that in mind, I pulled down the source for the “linux drivers” from Canon ran a bunch of configure scripts (remembering to use gcc, and adding LDFLAGS=”-lnsl -lsocket” to the env) and got started. All proceeded reasonably well until I got to what turns out to be the actual kernel of the problem: cnijfilter. This utility is the one which takes ghostscript’s ppmraw output and turns it into Canon-specific control codes which you can then send across the wire using cups-bjnp.

To get this to compile (as distinct from linking, more on that shortly), I had to make these changes to cnijfilter/src/getipc.c:

44c44
<     struct sockaddr_un        sun;
-
>     struct sockaddr_un    cifsun;
55,57c55,57
<     sun.sun_family = AF_UNIX;
<     strncpy(sun.sun_path, sname, sizeof(sun.sun_path) );
<     sun.sun_path[sizeof(sun.sun_path) - 1] = ‘\0′;
-
>     cifsun.sun_family = AF_UNIX;
>     strncpy(cifsun.sun_path, sname, sizeof(cifsun.sun_path) );
>     cifsun.sun_path[sizeof(cifsun.sun_path) - 1] = ‘\0′;
59c59
<     adrlen = sizeof(sun.sun_family) + strlen(sun.sun_path);
-
>     adrlen = sizeof(cifsun.sun_family) + strlen(cifsun.sun_path);
61c61
<     if (bind(s, (struct sockaddr *)&sun, adrlen))
-
>     if (bind(s, (struct sockaddr *)&cifsun, adrlen))
67c67
<     while ((c = accept(s, (struct sockaddr *)&sun, &adrlen)) >= 0) {
-
>     while ((c = accept(s, (struct sockaddr *)&cifsun, &adrlen)) >= 0) {

which helped with some symbol name clashes (at least, it did prior to build 143).

That left me with the binary blob linking problems. Fortunately, Canon actually supplies libraries which do the real work of the translation. Unfortunately (for me), those libraries all seemed to need linux’ libc.so.6, libdl.so.2 and libpthread.so.0. There are two specific symbols which they required: __errno_location(), and stderr. Now this I could do something about.

The link phase for the binary throws up these errors:

$   /opt/SUNWspro/SS12/bin/cc -O2 -L../../319/libs_bin -o cif bjferror.o bjfilter.o bjfimage.o bjfoption.o bjfpos.o bjfrcaccess.o getipc.o bjflist.o -lcnbpcmcm319 -lcnbpess319 -lm -ldl -ltiff -lpng -lcnbpcnclapi319 -lcnbpcnclbjcmd319 -lcnbpcnclui319 -lpopt
ld: warning: file libc.so.6: required by ../../319/libs_bin/libcnbpcmcm319.so, not found
ld: warning: file libpthread.so.0: required by ../../319/libs_bin/libcnbpess319.so, not found
ld: warning: file libdl.so.2: required by ../../319/libs_bin/libcnbpess319.so, not found
Undefined   first referenced
symbol      in file
bind        getipc.o
accept      getipc.o
listen      getipc.o
socket      getipc.o
stderr      ../../319/libs_bin/libcnbpcnclapi319.so
__errno_location ../../319/libs_bin/libcnbpcmcm319.so
ld: fatal: symbol referencing errors. No output written to cif
gmake: *** [cif] Error 1

Which is annoying at best. To get past this problem, I crafted up this little file:

#include <stdio.h>
int *
__errno_location(void) {
    return (int *)NULL;
}

#define stdin           (&_iob[0])
#define stdout          (&_iob[1])
#define stderr          (&_iob[2])

which I called fakery.c. I also created this mapfile to use when linking it:

$mapfile_version 2
SYMBOL_SCOPE {
    global:
        __errno_location;
        stderr;
};
SYMBOL_VERSION GLIBC_2.3.2 {} GLIBC_2.1.3;
SYMBOL_VERSION GLIBC_2.1.3 {} GLIBC_2.1;
SYMBOL_VERSION GLIBC_2.1 {} GLIBC_2.0 ;
SYMBOL_VERSION GLIBC_2.0 {} GLIBC_PRIVATE;
SYMBOL_VERSION GLIBC_PRIVATE { local: *;};

I then compiled fakery.c using

cc -G -Kpic -M MAPFILE.libc -R/opt/local/lib/canon -o libc.so.6 fakery.c

Lo and behold, I now had a libc.so.6:

$ file libc.so.6
libc.so.6:    ELF 32-bit LSB dynamic lib 80386 Version 1, dynamically linked, not stripped

with the requisite symbols available:

$ nm libc.so.6 |egrep "WEAK|GLOB"
[55]    |      1632|        28|FUNC |GLOB |0    |8      |__errno_location
[53]    |     67268|         0|OBJT |GLOB |0    |13     |_DYNAMIC
[46]    |     67532|         0|OBJT |GLOB |0    |16     |_edata
[50]    |     67536|         0|OBJT |GLOB |0    |17     |_end
[52]    |

      1720|         0|OBJT |GLOB |0    |11     |_etext
[51]    |     67256|         0|OBJT |GLOB |3    |12     |_GLOBAL_OFFSET_TABLE_
[54]    |         0|         0|OBJT |GLOB |0    |ABS    |_PROCEDURE_LINKAGE_TABLE_
[47]    |         0|         0|OBJT |WEAK |0    |ABS    |GLIBC_2.0
[48]    |         0|         0|OBJT |WEAK |0    |ABS    |GLIBC_2.1
[49]    |         0|         0|OBJT |WEAK |0    |ABS    |GLIBC_2.1.3
[43]    |         0|         0|OBJT |WEAK |0    |ABS    |GLIBC_2.3.2
[44]    |         0|         0|OBJT |GLOB |0    |ABS    |GLIBC_PRIVATE
[45]    |     67532|         4|OBJT |GLOB |0    |17     |stderr

Yay. Now for lib_dl.c and lib_pthread.c (the same file), and their mapfile:

$mapfile_version 2
SYMBOL_VERSION GLIBC_2.3.2 {} GLIBC_2.1.3;
SYMBOL_VERSION GLIBC_2.1.3 {} GLIBC_2.1;
SYMBOL_VERSION GLIBC_2.1 {} GLIBC_2.0 ;
SYMBOL_VERSION GLIBC_2.0 {} GLIBC_PRIVATE;
SYMBOL_VERSION GLIBC_PRIVATE { local: *;};

So I could do the same trick to generate libdl.so.2 and libpthread.so.0:

cc -G -Kpic -M MAPFILE.libs -R/opt/local/lib/canon -o libdl.so.2 otherlibs.c

Then I re-ran the cnijfilter link stage again, by hand:

$ /opt/SUNWspro/SS12/bin/cc -lnsl -lsocket -O2 -L../../319/libs_bin \
    -o cif bjferror.o bjfilter.o bjfimage.o bjfoption.o bjfpos.o \
    bjfrcaccess.o getipc.o bjflist.o -lcnbpcmcm319 -lcnbpess319 \
    -lm -ldl -ltiff -lpng -lcnbpcnclapi319 -lcnbpcnclbjcmd319 \
    -lcnbpcnclui319 -lpopt **-R/opt/local/lib/canon -L../../FAKE/**

Note the last bit – I’m adding an rpath (runtime path) so the binary will search for the libraries in /opt/local/lib/canon, and I added the directory for these fake libraries to the linker search path. Now the bits compile and link, and with an LD_LIBRARY_PATH entry in my environment I can see all the required libraries being found:

$ LD_LIBRARY_PATH=/opt/local/lib/canon ldd cif
    libnsl.so.1 =>     /lib/libnsl.so.1
    libsocket.so.1 =>     /lib/libsocket.so.1
    libcnbpcmcm319.so =>     /opt/local/lib/canon/libcnbpcmcm319.so
    libcnbpess319.so =>     /opt/local/lib/canon/libcnbpess319.so
    libm.so.2 =>     /lib/libm.so.2
    libdl.so.1 =>     /lib/libdl.so.1
    libtiff.so.3 =>     /usr/lib/libtiff.so.3
    libpng12.so.0 =>     /usr/lib/libpng12.so.0
    libcnbpcnclapi319.so =>     /opt/local/lib/canon/libcnbpcnclapi319.so
    libcnbpcnclbjcmd319.so =>     /opt/local/lib/canon/libcnbpcnclbjcmd319.so
    libcnbpcnclui319.so =>     /opt/local/lib/canon/libcnbpcnclui319.so
    libpopt.so.0 =>     /usr/lib/libpopt.so.0
    libc.so.1 =>     /lib/libc.so.1
    libmp.so.2 =>     /lib/libmp.so.2
    libmd.so.1 =>     /lib/libmd.so.1
    libc.so.6 =>     /opt/local/lib/canon/libc.so.6
    libdl.so.2 =>     /opt/local/lib/canon/libdl.so.2
    libpthread.so.0 =>     /opt/local/lib/canon/libpthread.so.0
    libjpeg.so.62 =>     /usr/lib/libjpeg.so.62
    libz.so.1 =>     /lib/libz.so.1

Still needed a wrapper script with it otherwise those binary blob libraries in /opt/local/lib/canon would fail to find libc.so.6:

#!/bin/bash
LD_LIBRARY_PATH=/opt/local/lib/canon exec /opt/local/bin/cifmp610.bin $*

But otherwise it seemed to be a reasonable start. It took a few reads of the Linker and Libraries Guide, and some back-n-forth help from the linker aliens but now I had a working binary translator.

Next step – cups-bjnp, but this actually turned out to be the bit which stopped me in my tracks.

I could hack the source ok to make it compile using getifaddrs:

558a559
> #ifndef __sun
562a564,569
> #else
>     if ((interface->ifa_addr == NULL) || (interface->ifa_broadaddr == NULL) ||
>         (interface->ifa_addr->ss_family != AF_INET) ||
>         (((struct sockaddr_in *) interface->ifa_addr)->sin_addr.s_addr ==
>         htonl(INADDR_LOOPBACK)))
> #endif

But it turns out that the Solaris behaviour is different to the linux behaviour – and I just do not have the time or energy to actually go and figure out how the packets are being misinterpreted. Yes, I snooped a bunch of the network traffic, but I’m stumped.

So I’m giving up on this printer, and Canon’s “open source” linux bits (did I mention they ship under GPLv2?). Until I can get my bones together to get an Epson (tx810fw is in the linuxprinting.org fully supported list, and Epson’s source for their translator is all open), I’ll run up my archlinux vbox and print using that instead.

I’m not going to buy another Canon printer, and I strongly suggest that if you want to have a printer which works under an open source operating system (using CUPS, of course) then you check out what is listed at www.linuxprinting.org/printers before making a purchase.


I’d like to thank Rod Evans, Ali Bahrami and Enrico Perla for all their help and patience with trying to get this figured out. With their help I’ve learnt quite a bit more about our linker capabilities, and the utilities (elfedit, elfdump, lari, crle and moe amongst others) which they’ve written to make it possible for me to turn code into something that runs.