Pimpl – Destruct Partial Declared Object

Pimpl is a common pattern in C++ design. It hides a class implementation details and keeps the public/protected interface unchanged when the implementation is changed.

Following codes illustrate how Pimpl pattern is implemented

// a.h
class A {
public:
     A();
     ~A();
public:
     void foo();                 // public/protected interfaces
private:
    class Pimpl;                 // partial (forward declare)
    friend class Pimpl;
    Pimpl* self;                 // opaque pointer
                                 // hold all private member/method
};

The implementation:

// a.cpp
#include "a.h"

// private implementation is declared in a.cpp only,
// changing a.cpp without a.h won't require re-compiling
// modules that reference a.h
class A::Pimpl {
    friend class A;
private:
    Pimpl() {}
    ~Pimpl() {}

    void foo() {}          // implementation of foo()

private:
    int props;              // private members of A
};

A::A()
 : self(new Pimpl())
{
}
A::~A()
{
    delete self;
}
void A::foo()
{
    self.foo();
}

When smart pointer is used, the code is changed like this

// a.h
   A();
   class Pimpl;
   std::unique<Pimpl> self;

// a.cpp
A::A() : self(new Pimpl()) {}

It wishes Pimpl* object is deleted automatically by smart pointer, so A::~A is not needed. Unfortunately, such code can not get compiled. Because A::Pimpl* is an opaque pointer (partial declared class) outside of a.cpp, it can NOT be created nor deleted outside of a.cpp.

Here is the proper implementation

// a.h
   A();
   ~A();       // do NOT declare as ~A() {}
   class Pimpl;
   std::unique<Pimpl> self;

// a.cpp
A::A() : self(new Pimpl()) {}  
A::~A() {}      // or in C++11 A::~A = default;

Because A::~A is defined in a.cpp, the compiler knows how to delete Pimpl* object.

 

 

 

Print JavaScript Object Recursively

It is convenient to use this small function to dump “any” object during debug.


function PPO(o, depth, indent) {
    var out = '';
    if (typeof indent === 'undefined') indent = 0;
    if (typeof depth === 'undefined') depth = -1;
    var sindent = ' '.repeat(4 * indent);
    if (typeof o === 'function') {
        out += 'function ' + o.toString();
    } else if (typeof o == 'number') {
        out += '' + o;
    } else if (typeof o == 'string') {
        out += '"' + o + '"';
    } else if (o instanceof Date) {
        out += 'Date "' + o.toISOString() + '"';
    } else {
        var isArray = o instanceof Array;
        if (depth == 0) {
            out += (isArray ? '[ ... ]' : '{ ... }');
        } else {
            out += (isArray ? '[\n' : '{\n');
            var i = 0;
            for (var n in o) {
                if (o.hasOwnProperty(n) /* && (typeof o[n] != 'function')*/) {
                    if (i++) out += ',\n';
                    out += sindent + ' ' + n + ': ' + PPO(o[n], depth - 1, indent + 1);
                }
            }
            out += '\n' + sindent + (isArray ? ']' : '}');
        }
    }
    return out;
}

 

In fact there is a simpler solution using JSON strignify.

function stringify (o) {
return JSON.strignify(o, null, 2)
}

 

Create Minidump in Program

Outlines

  1. install unhandled exception filter at the begin of program
  2. in the exception handle load dbghelp.dll
  3. invoke MiniDumpWriteDump

Sample Code


#include <Windows.h>
#include <DbgHelp.h>
#include <iostream>
#include <stdexcept>

typedef BOOL(WINAPI *MINIDUMPWRITEDUMP)
(HANDLE hProcess, DWORD dwPid, HANDLE hFile,
MINIDUMP_TYPE DumpType,
CONST PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
CONST PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
CONST PMINIDUMP_CALLBACK_INFORMATION CallbackParam);

LONG WINAPI MyExceptionFilter(struct _EXCEPTION_POINTERS* exceptionInfo)
{
    /* Do only necessary works in the exception handler.
       the program is not in stable status, don't do any fancy works.
       Fork a new process to perform complicated tasks.
    */
    _tprintf(_T("MyExceptionFilter is called\n"));

    // load dll by itself because:
    // * heap might be crashed
    // * don't need to link with dbghelp.lib

    HMODULE mhLib = ::LoadLibrary(_T("dbghelp.dll"));
    if (NULL == mhLib)
        _tprintf(_T("Fail to load library dbghelp.dll. Error: %u\n"), GetLastError());

    MINIDUMPWRITEDUMP pDump = (MINIDUMPWRITEDUMP)::GetProcAddress(mhLib, "MiniDumpWriteDump");
    if (NULL == pDump)
        _tprintf(_T("Fail to get MiniDumpWriteDump from DLL. Error: %u\n"), GetLastError());

    HANDLE hFile = CreateFile(_T("MyMiniDump.dmp"),
        GENERIC_READ | GENERIC_WRITE,
        0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

    if ((NULL != hFile) && (INVALID_HANDLE_VALUE != hFile))
    {
        MINIDUMP_EXCEPTION_INFORMATION ExInfo;
        ExInfo.ThreadId = GetCurrentThreadId();
        ExInfo.ExceptionPointers = exceptionInfo;
        ExInfo.ClientPointers = FALSE;

        BOOL rv = pDump(GetCurrentProcess(), GetCurrentProcessId(),
            hFile, MiniDumpNormal, &ExInfo, 0 /* userStream */, 0 /* callback */);

        if (!rv)
            _tprintf(_T("MiniDumpWriteDump failed. Error: %u\n"), GetLastError());
        else
            _tprintf(_T("MyMiniDump.dmp is saved\n"));

        CloseHandle(hFile);
    }
    else
    {
        _tprintf(_T("CreateFile failed. Error: %u\n"), GetLastError());
    }

    //return EXCEPTION_CONTINUE_SEARCH;     // continue handle search - default dialog: debug, cancel?
    return EXCEPTION_EXECUTE_HANDLER;       // exit immediately
}

/* sample codes that crashes */

using std::cout;
using std::endl;

void bar(int n)
{
    cout << "    bar starts " << n << endl;
    if (n == 0)
    {
        int *p = NULL;
        *p = 1;         // crash me!
    }
    cout << "    bar ends" << endl;
}

void foo(int n)
{
    cout << "=========== foo starts " << n << endl;
    bar(n);
    cout << "foo ends" << endl << endl;
}

int main()
{
    cout << "The play starts" << endl;

    // install the handle that generate minidump when crashes
    SetUnhandledExceptionFilter(MyExceptionFilter);

    try
    {
        foo(1);
        foo(0);    // will crash
        foo(2);
    }
    catch (const exception& e)
    {
        cout << "Exception: " << e.what() << endl;
    }
    cout << "The End" << endl;
    return 0;
}

Reference

 

Enable Core Dump On Linux

Outlines

  1. setup core-dump limit
  2. setup core-pattern
  3. prepare folder for core-dumps
  4. check bash limit is removed
  5. verify that everything works

 

Setup Resource Limit

First we need to setup resource limit at system-wide level.

  • edit /etc/security/limits.conf, add or un-comment this line
    *        soft    core    unlimited   # or number in KB
  • (optional) edit /etc/init.d/functions and add this line
    ulimit -S -c ${daemon_corefile_limit:-0} >/dev/null 2>&1
  • (centos/redhad only) edit /etc/sysconfig/init to enable core-dump globally
    DAEMON_COREFILE_LIMIT='unlimited'

 

Setup Core-Pattern

The current working directory is usually not a good place to save cores

  • the process might not have writing permission
  • the space might not enough
  • cwd might change during the running, and core might be dumped anytime
  • leave cores everywhere in the filesystem

Edit file /etc/sysctl.conf, add following lines

# setup core-pattern
fs.suid_dumpable = 1          # enable dumping suid app
kernel.core_uses_pid = 1      # append pid to the following string
kernel.core_pattern = /tmp/core  # core-dump prefix string
# the final core file will be /tmp/core.$pid

Let the settings take effect

sudo /sbin/sysctl -p

 

Prepare Folders for Cores

After setting core-patterns, don’t forget to create the folder that holds cores. The folder should be accessible for process that potentially create cores.

 

Check Bash limits

Resource limits of a process are inherited from its parent. The process can reduce but can’t increase the limit. Many shell profiles by default restrict core-dump ability. Check and remove/edit such restrictions like

ulimit -S -c 0 > /dev/null 2>&1

from profile of shell that launch the application

  • /etc/profile
  • ~/.profile
  • ~/.bashrc

 

Verify That Everything Works

  1. Logout and login
  2. Type ulimit -a to check core file size.
  3. Run any command and send it signal SIGSEGV while it is still running.

Debian vs Apline – aka glibc vs musl

Intro

Alpine is commonly chosen for light-weight docker container. However most applications are built on “normal” systems, i.e. glibc. Are programs compatible each other on two distributions?

 

About Alpine

Alpine is a minimal linux distro, based on musl. musl is a re-implementation of glibc, and is declared to be smaller, faster and more secure. Typical alpine docker image is 5mb (compare to debian 180mb).

$ docker pull alpine
$ docker pull frolvlad/alpine-gxx
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
centos latest 2d194b392dd1 13 days ago 195MB
frolvlad/alpine-gxx latest 1ef8d941aadd 2 weeks ago 151MB
alpine latest 3fd9065eaf02 2 months ago 4.15MB
hello-world latest f2a91732366c 3 months ago 1.85kB

 

Sample Programs

Here are two simple c/c++ programs. We are going to build them on debian and run on alpine, and vice versa.

cat t.c

#include <stdio.h>
int main()
{
 printf("Hello, world\n");
 return 0;
}

cat t.cpp

#include <iostream>
using namespace std;

int main()
{
cout << "Hello, this is c++" << endl;
return 0;
}

 

From Debian To Alpine

Nothing special to build those two programs. We build them staticly and dynamicly and see what is different.

jzou@debian9:~/tmp/exdk$ gcc -o t t.c
jzou@debian9:~/tmp/exdk$ gcc -o ts -static t.c
jzou@debian9:~/tmp/exdk$ g++ -o t+ t.cpp
jzou@debian9:~/tmp/exdk$ g++ -o t+s -static t.cpp
jzou@debian9:~/tmp/exdk$ ls -lh t*
-rwxr-xr-x 1 jzou jzou 8.5K Mar 19 16:04 t
-rwxr-xr-x 1 jzou jzou 9.1K Mar 19 16:04 t+
-rw-r--r-- 1 jzou jzou 73 Mar 19 13:07 t.c
-rw-r--r-- 1 jzou jzou 107 Mar 19 13:08 t.cpp
-rwxr-xr-x 1 jzou jzou 792K Mar 19 16:04 ts
-rwxr-xr-x 1 jzou jzou 2.0M Mar 19 16:04 t+s

 

Launch alpine docker and run those binaries.

docker run -it --name alpine -v ~/tmp/exdk:/home/jzou alpine
# cd /home/jzou
/home/jzou # ./t
/bin/sh: ./t: not found
/home/jzou # ./t+
/bin/sh: ./t+: not found
/home/jzou # ./ts
Hello, world
/home/jzou # ./t+s
Hello, this is c++

Go back to debian host, check dependencies

jzou@debian9:~/tmp/exdk$ ldd t
 linux-vdso.so.1 (0x00007ffe2fdd1000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fadd02b7000)
 /lib64/ld-linux-x86-64.so.2 (0x00007fadd0858000)
jzou@debian9:~/tmp/exdk$ ldd t+
 linux-vdso.so.1 (0x00007ffffa1d2000)
 libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4259f97000)
 libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4259c93000)
 libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4259a7c000)
 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f42596dd000)
 /lib64/ld-linux-x86-64.so.2 (0x00007f425a51b000)

Copy dependancies to alpine container

mkdir mylibs && cd mylibs
cp /lib64/ld-linux-x86-64.so.2 .
cp /lib/x86_64-linux-gnu/libc.so.6 .
cp /usr/lib/x86_64-linux-gnu/libstdc++.so.6 .
cp /lib/x86_64-linux-gnu/libgcc_s.so.1 .
cp /lib/x86_64-linux-gnu/libm.so.6 .
ls -lh
-rwxr-xr-x 1 jzou jzou 150K Mar 19 13:14 ld-linux-x86-64.so.2
-rwxr-xr-x 1 jzou jzou 1.7M Mar 19 13:12 libc.so.6
-rw-r--r-- 1 jzou jzou 91K Mar 19 13:18 libgcc_s.so.1
-rw-r--r-- 1 jzou jzou 1.1M Mar 19 13:17 libm.so.6
-rw-r--r-- 1 jzou jzou 1.5M Mar 19 13:17 libstdc++.so.6

Go apline container, set proper load and libc/c++

mkdir -p /lib64
cp mylibs/lib64/ld-linux-x86-64.so.2 /lib64
/home/jzou # ./t
./t: error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory
/home/jzou # export LD_LIBRARY_PATH=/home/jzou/mylibs
/home/jzou # ./t
Hello, world
/home/jzou # ./t+
Hello, this is c++

Well done!

 

From Alpine to Debian

Build programs on alpine with its gcc/g++, which are based on musl.

docker run -it --name agxx -v /home/jzou/tmp/exdk:/home/jzou "frolvlad/alpine-gxx"
# cd /home/jzou
/home/jzou # gcc -o at t.c
/home/jzou # ./at
Hello, world
/home/jzou # g++ -o at+ t.cpp
/home/jzou # ./at+
Hello, this is c++
/home/jzou # gcc -o ats -static t.c
/home/jzou # g++ -o at+s -static t.cpp
/home/jzou # ./ats
Hello, world
/home/jzou # ./at+s
Hello, this is c++
/home/jzou # ls -lh a*
-rwxr-xr-x 1 root root 10.4K Mar 19 23:27 at
-rwxr-xr-x 1 root root 11.0K Mar 19 23:27 at+
-rwxr-xr-x 1 root root 5.8M Mar 19 23:28 at+s
-rwxr-xr-x 1 root root 78.5K Mar 19 23:28 ats
/home/jzou # ls -lh t*
-rwxr-xr-x 1 1000 1000 8.4K Mar 19 23:04 t
-rwxr-xr-x 1 1000 1000 9.1K Mar 19 23:04 t+
-rwxr-xr-x 1 1000 1000 2.0M Mar 19 23:04 t+s
-rw-r--r-- 1 1000 1000 73 Mar 19 20:07 t.c
-rw-r--r-- 1 1000 1000 107 Mar 19 20:08 t.cpp
-rwxr-xr-x 1 1000 1000 791.7K Mar 19 23:04 ts

Back to debian host, try to run those binaries in debian

jzou@debian9:~/tmp/exdk$ ./at
bash: ./at: No such file or directory
jzou@debian9:~/tmp/exdk$ ./at+
bash: ./at+: No such file or directory
jzou@debian9:~/tmp/exdk$ ./ats
Hello, world
jzou@debian9:~/tmp/exdk$ ./at+s
Hello, this is c++

Like what we have done before, let’s copy load and support libraries to debian.

Back to alpine, find dependencies and grab them.

/home/jzou # ldd at
 /lib/ld-musl-x86_64.so.1 (0x7efdeef3c000)
 libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7efdeef3c000)
/home/jzou # ldd at+
 /lib/ld-musl-x86_64.so.1 (0x7f852e70b000)
 libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x7f852e1b7000)
 libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7f852e70b000)
 libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x7f852dfa5000)
/home/jzou # mkdir mymusl
/home/jzou # cp /lib/ld-musl-x86_64.so.1 mymusl
/home/jzou # cp /usr/lib/libstdc++.so.6 mymusl
/home/jzou # cp /usr/lib/libgcc_s.so.1 mymusl
/home/jzou # ls -lh mymusl
total 1964
-rwxr-xr-x 1 root root 550.5K Mar 20 00:04 ld-musl-x86_64.so.1
-rw-r--r-- 1 root root 69.7K Mar 20 00:09 libgcc_s.so.1
-rwxr-xr-x 1 root root 1.3M Mar 20 00:05 libstdc++.so.6

Go to debian

jzou@debian9:~/tmp/exdk$ sudo cp mylibs/ld-musl-x86_64.so.1 /lib
jzou@debian9:~/tmp/exdk$ export LD_LIBRARY_PATH=`pwd`/mymusl
jzou@debian9:~/tmp/exdk$ ./at
Hello, world
jzou@debian9:~/tmp/exdk$ ./at+
Hello, this is c++

Well done!

Conclusion

  • static-linked apps can run everywhere! Of course
  • dynamic-linked apps (by default) need to setup load lib and supporting libs (libc/c++)

Quick Steps on Docker

Install Docker

On debian

sudo apt-get update
sudo apt-get install \
 apt-transport-https \
 ca-certificates \
 curl \
 gnupg2 \
 software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository \
 "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") \
 $(lsb_release -cs) \
 stable"
sudo apt-get update
sudo apt-get install docker-ce
# apt-cache madison docker-ce
# verify everything is fine by launching a simple container
sudo docker run hello-world

Run docker command from non-root account

sudo groupadd docker
sudo usermod -aG docker jzou

 

Change Docker Storage

By default docker save all its data (images, containers, etc) at /var/lib/docker

use this command to check if it is not

docker info

Quite often there is enough space on the volume /var and you want to move it to some other mount. Here is the procedure that move data to /mnt/huge/docker

  1. Stop Docker
    systemctl stop docker
    systemctl daemon-reload
  2. Edit docker daemon configuration file /lib/systemd/system/docker.service
    FROM ExecStart=/usr/bin/dockerd
    TO ExecStart=/usr/bin/dockerd -g /mnt/huge/docker/
  3. Copy content
    rsync -aqxP /var/lib/docker/ /mnt/huge/docker/
  4. Restart docker
    systemctl start docker

Ref: https://stackoverflow.com/questions/32070113/how-do-i-change-the-default-docker-container-location

Pull Docker Images from Docker Hub

Here are some samples to

docker pull debian

In addition to some common base images such as CentOS, Debian/Ubuntu, some minimal linux distros are widely used, such as alpine, busybox

docker pull alpine

Start A Container

docker run -it --name cname docker_image [ command ]
  • -i          interactively
  • -t         bind with current terminal
  • –name give the container a name for easy reference (other than hash string)
  • command by default is specified by image, or /bin/bash for most OS images

 

Share Directory With Host

docker run --name cname -v host_path:container_path docker_image

 

 

Understanding Property Sheet In Visual Studio Project Management

Naive Editing Properties

The naive method to manage a Visual-Studio project for different configurations (configuration + platform, such as Debug+x64) is to modify values directly in the Property Editor for each configuration. The disadvantages are

  • duplication in multi-configurations, which is difficult to maintain the consistency.
  • difficult to apply to multiple users, or multiple similar projects.

The appropriated solution is to use customized property sheets, which support cascade overriding.

Two Ways to Access Property Editor

  • naive: from project explorer, you can access properties for different configurations
  • advanced: from property manager. Upper sheet in each configuration overrides values in lower sheet. For VS2015, the global sheets locate at <drive>\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V140, user sheets locates at <userprofile>\AppData\Local\Microsoft\MSBuild\v4.0. User sheets are obsolete, thus should be avoid to use (recommend to delete them from projects).

 

In the Property Manager, you can create/add any user custom property sheets at project level (that applies to all configuration) or at specified configuration.

custom-props.png

Project Property Sheet

Custom property sheets are saved in stand-alone XML files (for example, MyProps4All.props), instead of project files (.vcxproj). The advantage of stand-alone property sheet files is that they can be shared by different projects and configurations via importing.

The property inheritance (or override order) is

  1. Default settings from the MSBuild CPP Toolset (..\Program Files\MSBuild\Microsoft.Cpp\v4.0\Microsoft.Cpp.Default.props, which is imported by the .vcxproj file.)
  2. Property sheets
  3. .vcxproj file. (Can override the default and property sheet settings.)
  4. Items metadata

 

Reference

 

 

Displaying Chinese UTF-8 Characters in gvim On Windows

Following tip is copied from https://www.dzhang.com/blog/2013/04/02/displaying-chinese-utf-8-characters-in-gvim-on-windows

By default, gvim on my Windows machines just displays question marks, boxes, or garbled characters when I try to open files with Chinese text. The fix was rather simple:

  1. From the gettext project on SourceForge, get libiconv-1.9.1.bin.woe32.zip, which contains bin/iconv.dll. Put that file into gvim’s installation directory (for me, it’s C:\Program Files (x86)\Vim\vim73).
  2. Put this into vimrc:
    set encoding=utf8
    set guifontwide=NSimSun

Note: I’m using gvim 7.3.46 from the official site.

Credit goes to user Tobbe for this answer on superuser.com, which pointed out the key ingredient (iconv.dll).

 

Extra info

There is an extra vim plug-in that might be interesting, I haven’t verified it yet.

VimIM : Vim Input Method — Vim 中文输入法

Remote Debug With gdb/gdbserve

Overview

Remote debugging is useful or necessary, for example, in following scenarios:

  • There’s no full debugger on the target host, but a small stub (e.g. gdbserver) is available.
  • There’s no full source on the target host(for various reasons, such as size, security, etc). In a large project, synchronizing source codes on different hosts is either convenient nor safe. This is probably the most common case in the real world.
  • Debug input/output may pollute the target application input/output, for example, you are debugging a full-screen editor.

In practice, it’s better to have gdb and gdbserver with the same version. I had an experience that gdb (on centos5, v7.0.1) couldn’t connect to gdbserver (on debian 8, v7.7.1).

In the following example, the source codes locate on the host debian, the application is build and debug on debian, and the application is actually run on the remote host centos.

Prepare The Executable

No need to say, you need -g to keep symbols when you build your application. One additional tip: because the target application runs remotely, you don’t actually need to keep symbols in the target binary.

jason@debian$ gcc -g -o app app.c
jason@debian$ objcopy --only-keep-debug app app.debug
jason@debian$ strip -g -o app.remote app
jason@debian$ scp app.remote tony@centos:path/app

Explanation:

  1. build app with symbol
  2. extract symbols into a separate file. Now app.debug contains all debug information. This is common distribution practice.
  3. generate a version of executable without symbols, which (app.remote) is the version you actually distribute. The binary app still contains full symbols for debugger (you can, however, using app.remote plus app.debug, but what’s the point of that?)
  4. distribute the binary to the target host (host: centos, user: tony)

Launch At Target Host

On the target host (centos), start the application

tony@centos$ gdbserver localhost:4444 app
Process app created; pid = 10307
Listening on port 4444

The example uses tcp protocol. You can use serial com, in some special cases (such as in embedded system). The host part in host:port pair is not actually used in the current gdbserver version, so you can give it anything.

Start Debugging Session

Start debugging on the host debian. From the perspective of execution (on centos), gdb is running remotely (on debian); from the perspective of debugger, gdb (on debian) is debugging a program running on the remote host (centos).

jason@debian$ gdb app
... license info
Reading symbols from /home/jason/app...done.
(gdb) target remote centos:4444
Remote debugging using centos:4444
Reading symbos from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib64/ld-linux-x86-64.so.2
(gdb) break main
(gdb) run

Once gdb connects to gdbserver, you can debug as usual as a normal gdb session, such as step, break, print variables, etc. One thing to remember is that the input and output of the program happen on the remote host.

On the remote host (centos), the gdbserver session looks like

...
Listening on port 4444
Remote debugging from host 192.168.205.96

... normal application input/output

Child exited with status 0
GDBserver exiting
tony@centos$ 

Integrate Python and C/C++ (1)

In python official document there are two chapters with the topic of integrating python and C/C++

  • Extending and Embedding – tutorial for C/C++ programmer
  • Python/C API

In addition, there are several open-source utilities that make programmers’ lives easier, the most popular ones are

  • SWIG
  • Boost.Python/Pyl11
  • Cython
  • CFFI

This series posts explain the practical experience of the integration of python and C/C++.

Create a simple python module with C

The Python document gives a very simple example (spam), with full source code and detail explanation. What is missing is how you run on a real computer and what the output looks like.

Source Code

Here is the full source code with some concise comment

/* module method

 the name does not matter, and should be static, because the method
 is exposed by its implementation name, it is by method-define-array
 (see below)

 as practical convention, make your function name as module_method_name
*/

static PyObject*
spam_system(PyObject *self, PyObject *args)
{
 const char* command;

 int rc;

 if (!PyArg_ParseTuple(args, "s", &command))
 return NULL;

 rc = system(command);
 return Py_BuildValue("i", rc);
}

/*
 An array of PyMethodDef is passed to module initializer.
 This array defines all methods, each item is

 { name, function_pointer, argument_type, description }

 This array should also be static (no need to be exposed)
*/
static PyMethodDef SpamMethods[] = {
 { "system", spam_system, METH_VARARGS, "Execute a shell command." },

 // other methods

 // end of list
 {NULL, NULL, 0, NULL }
};


/*
 Each should have one (and only one) module initializer, that introduces
 module objects into python namespace.

 Its name MUST BE init.

 For example if the name is changed to init_spam, it still can be
 compiled but python can't import it:

python test.py echo hello
Traceback (most recent call last):
 File "test.py", line 4, in 
 import spam
ImportError: dynamic module does not define init function (initspam)

*/
PyMODINIT_FUNC
initspam(void)
{
 PyObject *m;

 m = Py_InitModule("spam", SpamMethods);
 if (m == NULL)
 return;
}

Manually build (on linux)

The document doesn’t mention the build process, instead it recommends to use distutils module. It is true, however, for curiosity I still decide to try to build manually. Here is the make file

$ cat manual.mk
# use pkg-config to find python information
#
# pkg-config --list-all
# pkg-config --cflags --libs python2
#
# -I/usr/include/python2.7 -I/usr/include/x86_64-linux-gnu/python2.7 -lpython2.7
CFLAGS = -g -fPIC -I/usr/include/python2.7
LDFLAGS = -g -shared -fPIC -L/usr/lib/python2.7
LIBS = -lpython2.7

all: spam.so

spam.o: spam.c
        $(CC) -c $(CFLAGS) -o $@ $<

spam.so: spam.o
        $(LD) -o $@ $< $(LDFLAGS) $(LIBS)

clean:
        rm spam.so spam.o

The build process

$ make -f manual.mk
cc -c -g -fPIC -I/usr/include/python2.7 -o spam.o spam.c
ld -o spam.so spam.o -g -shared -fPIC -L/usr/lib/python2.7 -lpython2.7

The python script that tests our new module spam

$ cat test.py
#! /usr/bin/env python

import sys,os
import spam

cmd = ' '.join(sys.argv[1:])
print 'cmd=[{}]'.format(cmd)
rc = spam.system(cmd)
print 'rc={:x}'.format(rc)

And the result of running.

$ python test.py hello
cmd=[hello]
sh: 1: hello: not found
rc=7f00
$ python test.py echo hello
cmd=[echo hello]
hello
rc=0

Proper method of building (on linux)

As python document recommends, it is much easier (and more standard and flexible) to build with distutils module

$ cat setup.py
#from distutils.core import setup, Extension
# distutils.core is obsolete, use setuptools instead
from setuptools import setup, Extension

# a package consists of one or more modules, each module consists of
# one or more source files
# this is module spam, built by source: spam.c
module1 = Extension('spam', sources = ['spam.c'])

# this is a package spam, consist of module1
setup(name='spam',
      version='1.0',
      description='This is a demo package',
      ext_modules=[module1])

The process that run with distutils

$ python setup.py build
running build
running build_ext
building 'spam' extension
creating build
creating build/temp.linux-x86_64-2.7
x86_64-linux-gnu-gcc -pthread -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fno-strict-aliasing -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -fPIC -I/usr/include/python2.7 -c spam.c -o build/temp.linux-x86_64-2.7/spam.o
creating build/lib.linux-x86_64-2.7
x86_64-linux-gnu-gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-z,relro -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security -Wl,-z,relro -D_FORTIFY_SOURCE=2 -g -fstack-protector-strong -Wformat -Werror=format-security build/temp.linux-x86_64-2.7/spam.o -o build/lib.linux-x86_64-2.7/spam.so
jasonz@jzdebian $ find build
build
build/lib.linux-x86_64-2.7
build/lib.linux-x86_64-2.7/spam.so
build/temp.linux-x86_64-2.7
build/temp.linux-x86_64-2.7/spam.o

Compare the output of the two build method

$ ls -l spam.o build/lib.linux-x86_64-2.7/spam.so
-rwxr-xr-x 1 jasonz jasonz 17304 Nov 30 13:46 build/lib.linux-x86_64-2.7/spam.so
-rw-r--r-- 1 jasonz jasonz 16752 Nov 30 13:49 spam.o
$ file spam.o build/lib.linux-x86_64-2.7/spam.so
spam.o:                             ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
build/lib.linux-x86_64-2.7/spam.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=9cdc9b1ebf2df10d243fbd931ecea38f3bd63dfc, not stripped

Build with distutils on Windows

Python 3.5 and later support VS2015 and later. For Python 2.7, Microsoft offers a special version of VC++, which is actually an engine of VC++2008.

Note:

  1. Python 2.7 for Windows is built by VC2008, this can be checked by launching python interpreter and check its build info, which should by MSC v.1500
  2. Without VC2008, setup.py auto detect and complains error “Unable to find vcvarsall.bat“.
  3. some old version of setup.py uses module distutils.core, which lacks the auto-detect ability, replace it with setuptools
  4. Microsoft requests to update setuptools to version 6.0 or later for letting the special version VC++ works
  5. download setuptools 6.0.1 ez_setup.py
  6. run command python ez_setup.py
Here is the process of build module on Windows.
D:\codex\python\ext>python setup.py build
running build
running build_ext
building 'spam' extension
creating build
creating build\temp.win-amd64-2.7
creating build\temp.win-amd64-2.7\Release
C:\Users\jasonz\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\VC\Bin\amd64\cl.exe /c /nologo /Ox /MD /W3 /GS- /DNDEBUG -IC:\Python27\include -IC:\Python27\PC /Tcspam.c /Fobuild\tem
p.win-amd64-2.7\Release\spam.obj
spam.c
creating build\lib.win-amd64-2.7
C:\Users\jasonz\AppData\Local\Programs\Common\Microsoft\Visual C++ for Python\9.0\VC\Bin\amd64\link.exe /DLL /nologo /INCREMENTAL:NO /LIBPATH:C:\Python27\libs /LIBPATH:C:\Python27\PCbuild\amd64 /LIBPA
TH:C:\Python27\PC\VS9.0\amd64 /EXPORT:initspam build\temp.win-amd64-2.7\Release\spam.obj /OUT:build\lib.win-amd64-2.7\spam.pyd /IMPLIB:build\temp.win-amd64-2.7\Release\spam.lib /MANIFESTFILE:build\tem
p.win-amd64-2.7\Release\spam.pyd.manifest
spam.obj : warning LNK4197: export 'initspam' specified multiple times; using first specification
   Creating library build\temp.win-amd64-2.7\Release\spam.lib and object build\temp.win-amd64-2.7\Release\spam.exp

D:\codex\python\ext>python test.py echo hello
cmd=[echo hello]
hello
rc=0

D:\codex\python\ext>python test.py  hello
cmd=[hello]
'hello' is not recognized as an internal or external command,
operable program or batch file.
rc=1

D:\codex\python\ext>