C/C++ Integration and Mixing
Jeff Marrison
Disclaimer: The views expressed here are solely those of the author, and do not necessarily represent the views of 2 Ton Digital.
Introduction
A couple of our clients have asked for code examples that mix C and C++ with my assembly language HeavyThing library, so I put together several examples here that cover the very very simple, through to SSH and networking using epoll. All of the examples listed here can be found in the HeavyThing examples directory and are now included with the main library itself. Today I am focussing primarily on making use of the HeavyThing library from within C and C++.
Table of Contents
- Forward and Basics
- Download Example Sources
- Hello World via C, include_everything
- Hello World via C, !include_everything
- Simple TCP Chat Server in C++
- Simple SSH Chat Server in C++
- Simple SSH Chat Server with SSH Auth in C++
Forward and Basics
Mixing assembly language with C and C++ is straightforward and simple thanks to the HeavyThing library's use of standard ABI calling conventions throughout. For those of you not already aware, linux x86_64 functions pass parameters as registers and their return is a single register. What this effectively means is that C, C++ and other higher level languages can have function declarations that are native to them regardless of the underlying language it is built in (provided of course the language is ABI compliant itself and not interpreted, bytecode, etc). Consider the following dummy function definitions in C:
void f1() {
}
int f2() {
}
long f3(const char *a) {
}
void *f4(const char *a, int b, long c) {
}
The linux x86_64 calling conventions dictate that arguments are passed in the following order: rdi, rsi, rdx, rcx, r8, r9, r10 (noting that syscall/kernel entries have a different order). Further, it specifies that the return for a function (if any) lands in rax. When double arguments or returns are included in the mix, they are dealt with similarly but ordered from xmm0 upward, with double returns occuring in xmm0. Looking at our prior C function definitions, we can then readily map the arguments and returns:
- f1: no arguments, no return.
- f2: no arguments, return value will be in
eax - f3: single argument in
rdi, return value inrax - f4: three arguments,
rdi, esi, rdx, return inrax
Conversely, and anywhere within the HeavyThing library where functions are defined, they are prefaced with comment descriptions of their arguments and return values if any such that we can do the reverse mapping in precisely the same way in order to achieve C declarations for them:
if used heap$alloc | defined include_everything ; single argument in rdi for count, returns in rax falign heap$alloc: prolog heap$alloc ...
That is the primary malloc equivalent for the HeavyThing library. We can map that directly to C using the same calling convention methodology:
void *heap$alloc(unsigned long);
These concepts are important mainly because I haven't taken the time to create and/or maintain a C header file for the library, but hopefully as you can see, doing the definitions for the functions that you need is trivial and straightforward.
Download Example Sources
All of the sources and binaries referenced on this page are now included in the examples directory of the HeavyThing library itself. The download link is located in the upper right of every page on this site. For these examples to work as expected, you will also need a functioning gcc/g++ installation along with fasm itself.
Hello World via C, include_everything
For our first contrived example, we make use of the HeavyThing's include_everything option located inside the main settings file ht_defaults.inc. When this setting is enabled, and any assembly language file is compiled that includes the HeavyThing library, ALL of its code gets compiled into a single object file. At the time of this writing, that amounts to about 790KB, and that includes all of the functions in the library. The downside to doing it this way is of course that all of the aforementioned 790KB binary ends up in our final executable (See the next example for how to avoid this). Anyway, our assembly language file ht.asm consists solely of:
include 'settings.inc' include '../../ht.inc' include '../../ht_data.inc'
To compile our ht.asm:
$ fasm -m 524288 ht.asm
That produces an x86_64 object file ht.o with the entirety of our library. For our simple example in C, our hello.c looks like:
/* declarations for HeavyThing functions that we need: */ void ht$init_args(int, char **); void *string$from_cstr(const void *); void string$to_stdoutln(const void *); void heap$free(const void *); int ht$syscall(int num); int main(int argc, char *argv[]) { /* First order of business: initialize HeavyThing */ /* Since we aren't interested in arguments/env, 0/NULL work fine here */ ht$init_args(0, 0); /* Next, since we can't use the HeavyThing cleartext macro to create a static string, we'll create one from a normal const char * */ void *s = string$from_cstr("Heya"); /* Write it */ string$to_stdoutln(s); /* Free it */ heap$free(s); /* since we compile with -nostdlib, make a call to exit (60) */ ht$syscall(60); }
To compile it into our final executable, we can compile with or without -nostdlib since in our contrived example we don't need any of what libc has to offer:
$ gcc -nostdlib -o hello hello.c ht.o
The important thing to note here is, drawing from our forward and basics section, all of the HeavyThing library's functions are accessible this way from inside C. Our use of include_everything when doing so has made a ridiculously sized binary for such a simple example however, which is what we'll cover in our second example.
Hello World via C, !include_everything
By default, the include_everything is so that each and every HeavyThing program needn't contain unreferenced functions in the library. We can achieve the same conditional compilation results by forcing fasm to make a reference to the functions we wish to include. This example is otherwise the same as the first, but our ht.asm now looks like:
include 'settings.inc' include '../../ht.inc' ; Unlike our first example, this time we don't want the ENTIRE ; library being included in our example program ; So, we create a dummy wrapper so that fasm only includes the ; functions that we want: _include: call ht$init_args call string$from_cstr call string$to_stdoutln call heap$free call ht$syscall include '../../ht_data.inc'
Our _include declaration here is never used or called, but lets fasm know to include those functions any any other functions that they depend on when it produces our binary. In this way, our resultant ht.o binary is a much more reasonable ~8KB (note that this includes full Unicode parsing/emitter, memory management, and VDSO) and does not contain unused functions any longer. We then compile the second example the same as the first:
$ fasm -m 524288 ht.asm $ gcc -nostdlib -o hello hello.c ht.o
Simple TCP Chat Server in C++
Our first two contrived examples did not make use of any higher level language features, so for this example we increase our example complexity considerably while still keeping with the same principles of including HeavyThing code. One of the many strengths of the HeavyThing library is in its epoll implementation. In this example, we provide a standard TCP-based telnet "chat server." The quotes there mainly mean that we didn't go to any lengths for our example's sake in making it a useful chat server, but it serves to highlight combining C++ memory managers, STL containers, and networking from the HeavyThing library.
Again like our second example, we don't want our resultant binary to include every last bit of the library, so our ht.asm again contains a dummy wrapper with the functions from the HeavyThing that we are after. Note that unlike our prior example, this time we didn't bother with using call lines for each function, and instead created only a table of function pointers for our _include function:
include '../../ht_defaults.inc' include '../../ht.inc' ; dummy wrapper so that our fasm compiler will include ; the parts of the HeavyThing library that we want (instead of ; everything which is bigger than it needs to be). ; NOTE: unlike our previous examples, this time we just define ; them all as data pointers which still forces fasm to include ; it all. _include: dq ht$init_args dq string$from_cstr dq string$to_stdoutln dq heap$alloc dq heap$free dq epoll$new dq inaddr_any dq epoll$inbound dq epoll$destroy dq epoll$clone dq epoll$receive dq epoll$send dq io$error dq io$timeout dq epoll$run dq inet_ntoa dq string$from_unsigned dq string$to_utf8 include '../../ht_data.inc'
Our simplechat.cpp file that covers our basic functionality:
#include <cstdio> #include <string> #include <iostream> #include <unordered_map> /* All our HeavyThing functions need C declarations (otherwise, name mangling and all sorts of fun happens). */ extern "C" { void ht$init_args(int, char **); void *epoll$new(void *, int); void epoll$destroy(void *); void *epoll$clone(void *); bool epoll$receive(void *, const void *, int); void epoll$send(void *, const void *, int); void simplechat_error(void *); bool io$timeout(void *); void inaddr_any(void *, int); bool epoll$inbound(void *, int, void *); void epoll$run(); void *inet_ntoa(unsigned); void *string$from_unsigned(unsigned, int); int string$to_utf8(void *, void *); }; /* Forward declarations for our custom simplechat functions so that they can be included in our HeavyThing virtual method table: */ void simplechat_connected(void *epoll_object, void *raddr, int raddr_len); bool simplechat_received(void *epoll_object, void *buffer, int len); void simplechat_error(void *epoll_object); /* The HeavyThing epoll layer works with what basically amounts to a virtual method table, but they are explicitly defined. Rather than wrap the HeavyThing functionality into a corresponding C++ layer, this example explicitly defines them the same as we'd do in our native assembly language environment. Note re: typecasting these, since all our HeavyThing functions require varying arguments, we have to forcibly typecast them to get them into a sensible array necessary for the virtual method table functionality that HeavyThing requires: */ typedef void (*vmethod_t)(void); static vmethod_t epoll_methods[7] = { (vmethod_t)epoll$destroy, (vmethod_t)epoll$clone, (vmethod_t)simplechat_connected, (vmethod_t)epoll$send, (vmethod_t)simplechat_received, (vmethod_t)simplechat_error, (vmethod_t)io$timeout }; /* Here we define our pathetically simple class to hold our state information. Note re: std::string use here: We are intentionally not using HeavyThing's string implementation despite it being UTF capable mainly to show that language mixing is trivial. */ class ChatClient { public: ChatClient(void *epoll, void *raddr, int raddrlen) : epoll(epoll) { /* Construct a std::string representation of our remote address. The actual 32 bit address in a sockaddr_in structure is located at +4, and the big endian port number is at +2. Since we don't want our compiler complaining about pointer math we case it to an unsigned char * */ unsigned char *r = (unsigned char *)raddr; void *ht_addrstr = inet_ntoa(*(unsigned *)(r+4)); /* Next up, we need the port number, but since it is in big endian order, we can extract the bytes directly. */ int port = (r[2] << 8) + r[3]; /* Convert that to a string with the HeavyThing string library... of course we could use snprintf, etc but we are mixing it up on purpose here. */ void *ht_portstr = string$from_unsigned(port, 10); /* Now we can construct our char * of the above */ char scratch[32]; int i = string$to_utf8(ht_addrstr, scratch); scratch[i] = ':'; int j = string$to_utf8(ht_portstr, &scratch[i+1]); scratch[i+j+1] = 0; remote_address = std::string(scratch, i+j+1); } ~ChatClient() { } void *epoll; std::string remote_address; std::string handle; }; /* Our typedefs and global static client map: */ typedef std::pair<void *, ChatClient *> clients_r; typedef std::unordered_map<void *, ChatClient *> clients_t; static clients_t clients; /* This function is called by the HeavyThing's epoll layer when a new connection arrives. We are passed an epoll object (specific to the connection itself), and its remote address details. Our only mission here is to send a greeting to the client and add it to our clients map. */ void simplechat_connected(void *epoll_object, void *raddr, int raddr_len) { /* Send our greeting first. */ static const std::string greeting("Greetings! Your desired handle? "); epoll$send(epoll_object, greeting.c_str(), greeting.length()); /* Add the client to our map. */ clients.insert(clients_r(epoll_object, new ChatClient(epoll_object, raddr, raddr_len))); } /* When data arrives from a connection, this function is called. If we return true from here, the connection will be closed, false and it stays open. */ bool simplechat_received(void *epoll_object, void *buffer, int len) { /* Truncate any linefeeds on the end of what we received: */ const char *b = (const char *)buffer; while (len && (b[len-1] == '\r' || b[len-1] == '\n')) len--; /* Ignore empty lines. */ if (!len) return false; /* For our simple chat server example, turn what we got into a std::string: */ std::string input(b, len); /* Thanks to the automatic buffering that our HeavyThing epoll layer does for us, we have to drain it by calling the default epoll$receive object (the default ONLY drains) */ epoll$receive(epoll_object, buffer, len); /* Find our client in our map: */ clients_t::const_iterator x = clients.find(epoll_object); /* Sanity only: freak out if we failed. */ if (x == clients.end()) return true; ChatClient *client = (*x).second; /* Our only two states are: 1) very first message, call that their handle. 2) anything else, normal message. */ if (client->handle.empty()) { /* Set our client handle */ client->handle = input; /* Send back a greeting specific to our client. */ static const std::string hello_part1("Welcome, "); static const std::string hello_part2("\nEverything to you type will be " "broadcast to other connected clients,\nand will not be echoed " "back to you.\n\nCommands we understand are: /who and /exit\n"); std::string greeting = hello_part1 + input + hello_part2; epoll$send(epoll_object, greeting.c_str(), greeting.length()); /* Let everyone else know our client has arrived, but only those who have already provided a handle. */ static const std::string arrival = " has joined the circus.\n"; std::string notice = input + arrival; x = clients.begin(); while (x != clients.end()) { if ((*x).first != epoll_object && !(*x).second->handle.empty()) epoll$send((*x).first, notice.c_str(), notice.length()); x++; } /* Done, return false to keep the connection open. */ return false; } /* Otherwise, normal message. Check for our two known commands, else broadcast. */ if (input == "/who") { /* Construct a std::string of everyone here, including the asker. */ std::string response = "Smoke on the water: "; std::string present; clients_t::const_iterator x = clients.begin(); while (x != clients.end()) { if (!present.empty()) present += ", "; present += (*x).second->handle; present += " ("; present += (*x).second->remote_address; present += ")"; x++; } response += present; response += "\n"; epoll$send(epoll_object, response.c_str(), response.length()); /* Return false to keep the connection open. */ return false; } if (input == "/exit") { static const std::string bye = "Bye.\n"; epoll$send(epoll_object, bye.c_str(), bye.length()); /* Let everyone else know, noting here that since this also occurs from an epoll error event, we separated it out into its own function (will clean itself up too). */ simplechat_error(epoll_object); /* return true from here to kill the current connection. */ return true; } /* Normal broadcast message, send to everyone except the sender. */ std::string message = client->handle; message += ": "; message += input; message += "\n"; x = clients.begin(); while (x != clients.end()) { if ((*x).first != epoll_object) epoll$send((*x).first, message.c_str(), message.length()); x++; } /* Return false to keep the connection open. */ return false; } /* This function is called either by an /exit command from a client, or on epoll error when they disconnected. Notify everyone else that this client has left, but only if they already provided a handle. */ void simplechat_error(void *epoll_object) { clients_t::const_iterator x = clients.find(epoll_object); /* Sanity only, bailout if we didn't find it */ if (x == clients.end()) return; std::string handle = (*x).second->handle; /* Remove them from our map. */ delete (*x).second; clients.erase(x); /* Let everyone remaining who has a handle know they left. */ handle += " has departed.\n"; x = clients.begin(); while (x != clients.end()) { if (!(*x).second->handle.empty()) epoll$send((*x).first, handle.c_str(), handle.length()); x++; } } int main(int argc, char *argv[]) { /* First up: initialize HeavyThing, and like our previous examples, we are not interested in argc/argv from inside our assembler environment: An interesting sidenote here: because HeavyThing has normal function definitions for memcpy, memcmp, etc, those get preferentially linked instead of the libc versions, and so during our C++ initialization, those HeavyThing functions were already called. Fortunately for us in this case, those functions do not require any HeavyThing global state. */ ht$init_args(0, 0); /* Create a HeavyThing epoll listener object */ void *listener = epoll$new(epoll_methods, 0); /* Setup an IPv4 socket address for our listener, noting that sockaddr_in_size from epoll.inc is 16 bytes. Listener port == 8001: */ unsigned char addrbuf[16]; inaddr_any(addrbuf, 8001); /* Now we can pass that off to our epoll layer. The HeavyThing's epoll$inbound will return false if bind failure: */ if (!epoll$inbound(addrbuf, sizeof(addrbuf), listener)) { std::cerr << "INADDR_ANY:8001 bind failure." << std::endl; exit(1); } /* Dump a banner to stdout so that we know all is well */ std::cout << "Simple chat server listening on port 8001." << std::endl; /* Pass control (indefinitely) to HeavyThing's epoll layer. */ epoll$run(); /* Not reached. */ }
The comments throughout the code tell the story nicely, but particular care should be taken when reviewing the extern "C" HeavyThing library function definitions. When working with C++, this is required to prevent g++ from name mangling all of them. Using the same techniques in reverse allows us to create functions with names that match g++ name mangling and provide pure assembler routines for otherwise C++-only code. To compile our simplechat.cpp program:
$ fasm -m 524288 ht.asm $ g++ -std=c++11 -o simplechat simplechat.cpp ht.o
Simple SSH Chat Server in C++
The previous TCP Chat Server example made use of the HeavyThing library's epoll layer directly. That is to say that it did not make use of the inbuilt "IO layers" that the epoll functionality provides. This example behaves the same way as our previous example, but instead of plain TCP, this presents the interface via an SSH server. The primary difference between this and the last example is that the "IO layering" is used instead of direct epoll calls, and that unlike telnet based connections, SSH connections do not receive linemode input and no echoing is performed. As a result, we go through a bit more effort to provide the same interface and functionality via SSH that we did with our first example. Thanks to our use of a different set of HeavyThing library functions, our ht.asm now looks like:
include '../../ht_defaults.inc' include '../../ht.inc' ; dummy wrapper so that our fasm compiler will include ; the parts of the HeavyThing library that we want (instead of ; everything which is bigger than it needs to be). ; NOTE: unlike our previous examples, this time we just define ; them all as data pointers which still forces fasm to include ; it all. _include: dq ht$init_args dq string$from_cstr dq string$to_stdoutln dq heap$alloc dq heap$free dq epoll$new dq inaddr_any dq epoll$inbound dq io$destroy dq io$clone dq epoll$receive dq epoll$send dq io$error dq io$timeout dq epoll$run dq inet_ntoa dq string$from_unsigned dq string$to_utf8 dq io$new dq ssh$new_server dq io$send dq epoll$default_vtable dq io$link include '../../ht_data.inc'
And our simplechat_ssh.cpp file:
#include <cstdio> #include <string> #include <iostream> #include <unordered_map> /* This example is the same as our previous C++ simplechat, only we do it all via SSH2. There are some pretty hefty differences between line-based telnet-friendly plain and the foundation for a proper SSH terminal session, mainly that instead of getting line-at-a-time, we get character at a time, and we have to echo each character back to the user. And this example makes use of the HeavyThing IO layering which is required for SSH/TLS/etc. */ /* All our HeavyThing functions need C declarations (otherwise, name mangling and all sorts of fun happens). */ typedef void (*vmethod_t)(void); extern "C" { void ht$init_args(int, char **); void **io$new(); void *ssh$new_server(void *); void *epoll$new(vmethod_t, int); void io$destroy(void *); void *io$clone(void *); void io$send(void *, const void *, int); void simplechat_error(void *); bool io$timeout(void *); void inaddr_any(void *, int); bool epoll$inbound(void *, int, void *); void epoll$run(); void *inet_ntoa(unsigned); void *string$from_unsigned(unsigned, int); int string$to_utf8(void *, void *); void io$link(void *, void *); void epoll$default_vtable(); }; /* Our default vtable for the epoll layer is a public symbol from the HeavyThing library: */ /* Forward declarations for our custom simplechat functions so that they can be included in our HeavyThing virtual method table: */ void simplechat_connected(void *epoll_object, void *raddr, int raddr_len); bool simplechat_received(void *epoll_object, void *buffer, int len); void simplechat_error(void *epoll_object); /* The HeavyThing epoll layer works with what basically amounts to a virtual method table, but they are explicitly defined. Rather than wrap the HeavyThing functionality into a corresponding C++ layer, this example explicitly defines them the same as we'd do in our native assembly language environment. Note re: typecasting these, since all our HeavyThing functions require varying arguments, we have to forcibly typecast them to get them into a sensible array necessary for the virtual method table functionality that HeavyThing requires: */ static vmethod_t epoll_methods[7] = { (vmethod_t)io$destroy, (vmethod_t)io$clone, (vmethod_t)simplechat_connected, (vmethod_t)io$send, (vmethod_t)simplechat_received, (vmethod_t)simplechat_error, (vmethod_t)io$timeout }; /* Here we define our pathetically simple class to hold our state information. Note re: std::string use here: We are intentionally not using HeavyThing's string implementation despite it being UTF capable mainly to show that language mixing is trivial. */ class ChatClient { public: ChatClient(void *epoll, void *raddr, int raddrlen) : epoll(epoll) { /* Construct a std::string representation of our remote address. The actual 32 bit address in a sockaddr_in structure is located at +4, and the big endian port number is at +2. Since we don't want our compiler complaining about pointer math we case it to an unsigned char * */ unsigned char *r = (unsigned char *)raddr; void *ht_addrstr = inet_ntoa(*(unsigned *)(r+4)); /* Next up, we need the port number, but since it is in big endian order, we can extract the bytes directly. */ int port = (r[2] << 8) + r[3]; /* Convert that to a string with the HeavyThing string library... of course we could use snprintf, etc but we are mixing it up on purpose here. */ void *ht_portstr = string$from_unsigned(port, 10); /* Now we can construct our char * of the above */ char scratch[32]; int i = string$to_utf8(ht_addrstr, scratch); scratch[i] = ':'; int j = string$to_utf8(ht_portstr, &scratch[i+1]); scratch[i+j+1] = 0; remote_address = std::string(scratch, i+j+1); } ~ChatClient() { } void *epoll; std::string remote_address; std::string handle; std::string accum; }; /* Our typedefs and global static client map: */ typedef std::pair<void *, ChatClient *> clients_r; typedef std::unordered_map<void *, ChatClient *> clients_t; static clients_t clients; /* This function is called by the HeavyThing's epoll layer when a new connection arrives. We are passed an epoll object (specific to the connection itself), and its remote address details. Our only mission here is to send a greeting to the client and add it to our clients map. */ void simplechat_connected(void *epoll_object, void *raddr, int raddr_len) { /* Send our greeting first. */ static const std::string greeting("Greetings! Your desired handle? "); io$send(epoll_object, greeting.c_str(), greeting.length()); /* Add the client to our map. */ clients.insert(clients_r(epoll_object, new ChatClient(epoll_object, raddr, raddr_len))); } /* When data arrives from a connection, this function is called. If we return true from here, the connection will be closed, false and it stays open. */ bool simplechat_line(void *epoll_object, ChatClient *client); bool simplechat_received(void *epoll_object, void *buffer, int len) { /* Unlike our telnet-friendly previous version, we get SSH2 input character-by-character, so we accumulate them until we get a linefeed. First up, find our client in our map: */ clients_t::const_iterator x = clients.find(epoll_object); /* Sanity only: freak out if we failed. */ if (x == clients.end()) return true; ChatClient *client = (*x).second; /* Since SSH disables echo, echo for the typist */ io$send(epoll_object, buffer, len); /* Loop through however many characters we got. */ const char *b = (const char *)buffer; for (int i = 0; i < len; i++) { const char c = b[i]; if (c == 13) { if (simplechat_line(epoll_object, client)) return true; // send an LF for them io$send(epoll_object, "\n", 1); client->accum.clear(); } else if (c == 127) { if (!client->accum.empty()) client->accum = client->accum.substr(0, client->accum.length()-1); io$send(epoll_object, "\b \b", 3); } else if (c != 10) { client->accum += c; } } return false; } /* Line-based input version from above: */ bool simplechat_line(void *epoll_object, ChatClient *client) { /* Ignore empty lines. */ if (client->accum.empty()) return false; std::string input = client->accum; /* Our only two states are: 1) very first message, call that their handle. 2) anything else, normal message. */ clients_t::const_iterator x; if (client->handle.empty()) { /* Set our client handle */ client->handle = input; /* Send back a greeting specific to our client. */ static const std::string hello_part1("\nWelcome, "); static const std::string hello_part2("\r\nEverything to you type will be " "broadcast to other connected clients,\r\nand will not be echoed " "back to you.\r\n\r\nCommands we understand are: /who and /exit\r"); std::string greeting = hello_part1 + input + hello_part2; io$send(epoll_object, greeting.c_str(), greeting.length()); /* Let everyone else know our client has arrived, but only those who have already provided a handle. */ static const std::string arrival = " has joined the circus.\r\n"; std::string notice = input + arrival; x = clients.begin(); while (x != clients.end()) { if ((*x).first != epoll_object && !(*x).second->handle.empty()) io$send((*x).first, notice.c_str(), notice.length()); x++; } /* Done, return false to keep the connection open. */ return false; } /* Otherwise, normal message. Check for our two known commands, else broadcast. */ if (input == "/who") { /* Construct a std::string of everyone here, including the asker. */ std::string response = "\nSmoke on the water: "; std::string present; clients_t::const_iterator x = clients.begin(); while (x != clients.end()) { if (!present.empty()) present += ", "; present += (*x).second->handle; present += " ("; present += (*x).second->remote_address; present += ")"; x++; } response += present; response += "\r"; io$send(epoll_object, response.c_str(), response.length()); /* Return false to keep the connection open. */ return false; } if (input == "/exit") { static const std::string bye = "\nBye.\r\n"; io$send(epoll_object, bye.c_str(), bye.length()); /* Let everyone else know, noting here that since this also occurs from an epoll error event, we separated it out into its own function (will clean itself up too). */ simplechat_error(epoll_object); /* return true from here to kill the current connection. */ return true; } /* Normal broadcast message, send to everyone except the sender. */ std::string message = client->handle; message += ": "; message += input; message += "\r\n"; x = clients.begin(); while (x != clients.end()) { if ((*x).first != epoll_object) io$send((*x).first, message.c_str(), message.length()); x++; } /* Return false to keep the connection open. */ return false; } /* This function is called either by an /exit command from a client, or on epoll error when they disconnected. Notify everyone else that this client has left, but only if they already provided a handle. */ void simplechat_error(void *epoll_object) { clients_t::const_iterator x = clients.find(epoll_object); /* Sanity only, bailout if we didn't find it */ if (x == clients.end()) return; std::string handle = (*x).second->handle; /* Remove them from our map. */ delete (*x).second; clients.erase(x); /* Let everyone remaining who has a handle know they left. */ handle += " has departed.\r\n"; x = clients.begin(); while (x != clients.end()) { if (!(*x).second->handle.empty()) io$send((*x).first, handle.c_str(), handle.length()); x++; } } int main(int argc, char *argv[]) { /* First up: initialize HeavyThing, and like our previous examples, we are not interested in argc/argv from inside our assembler environment: An interesting sidenote here: because HeavyThing has normal function definitions for memcpy, memcmp, etc, those get preferentially linked instead of the libc versions, and so during our C++ initialization, those HeavyThing functions were already called. Fortunately for us in this case, those functions do not require any HeavyThing global state. */ ht$init_args(0, 0); /* We need a default/generic io layer for our top level */ void **iolayer = io$new(); /* Set its virtual method table to our own: */ iolayer[0] = epoll_methods; /* Next in the layers is our ssh server layer, and its argument as 0 specifies to use /etc/ssh host keys */ void *sshserver = ssh$new_server(0); if (!sshserver) { std::cerr << "SSH host key error" << std::endl; exit(1); } /* We have to link the iolayer with the ssh layer: */ io$link((void *)iolayer, sshserver); /* And our lowest layer is a real epoll object with defaults */ void *listener = epoll$new((vmethod_t)epoll$default_vtable, 0); /* Link that to our sshserver */ io$link(sshserver, listener); /* Setup an IPv4 socket address for our listener, noting that sockaddr_in_size from epoll.inc is 16 bytes. Listener port == 8001: */ unsigned char addrbuf[16]; inaddr_any(addrbuf, 8001); /* Now we can pass that off to our epoll layer. The HeavyThing's epoll$inbound will return false if bind failure: */ if (!epoll$inbound(addrbuf, sizeof(addrbuf), (void *)iolayer)) { std::cerr << "INADDR_ANY:8001 bind failure." &<< std::endl; exit(1); } /* Dump a banner to stdout so that we know all is well */ std::cout << "Simple SSH chat server listening on port 8001." << std::endl; /* Pass control (indefinitely) to HeavyThing's epoll layer. */ epoll$run(); /* Not reached. */ }
$ fasm -m 524288 ht.asm $ g++ -std=c++11 -o simplechat_ssh simplechat_ssh.cpp ht.o
Simple SSH Chat Server with SSH Auth in C++
Our previous Simple SSH Chat Server did not present any SSH2 authentication methods to its clients. This example only differs from the previous one in that it sets up a HeavyThing SSH authentication callback function so that your SSH service can be securely authenticated (of course, our example authenticates ANY username and password combination so it is up to the reader to decide how/what to do there). Note that this function contains memory leaks and only serves our needs for demonstration purposes (read: fill in the rest yourself! haha!) Rather than dump redundant code that is nearly identical to the last example, here is the diff:
53a47 > typedef bool (*authcb_t)(void *, void *, void *); 59a54 > void ssh$set_authcb(void *, authcb_t); 71c66 < int string$to_utf8(void *, void *); --- > size_t string$to_utf8(void *, void *); 73a69,70 > void string$to_stdoutln(void *); > size_t string$utf8_length(void *); 380a378,395 > bool ssh_authenticate(void *sshclient, void *ht_username, void *ht_password) { > /* > Since both the strings we get are HeavyThing strings, we can > convert them to std::string's first. > */ > char *userbuf = (char *)malloc(string$utf8_length(ht_username)+1); > userbuf[string$to_utf8(ht_username, userbuf)] = 0; > char *passbuf = (char *)malloc(string$utf8_length(ht_password)+1); > passbuf[string$to_utf8(ht_password, passbuf)] = 0; > > std::string user(userbuf); > std::string pass(passbuf); > > std::cout << "Authenticating User: [" << user << "] Pass: [" << pass << "]" << std::endl; > > return true; > } > 411a427,430 > /* > Unlike our authless version, set our authentication callback > */ > ssh$set_authcb(sshserver, ssh_authenticate);