chickadee » feature-test

feature-test

feature-test provides foreign feature testing macros and read-syntax. This can be used to alter code at compile-time based on system support for a particular feature.

Overview

Feature testing is a time-honored tradition of C programmers who wish to write portable code.

First, the programmer probes the system with a utility such as configure, generating a header file with #defines such as HAVE_IPV6 or HAVE_ADDRINFO. The programmer may also manually generate a header file by examining the system (e.g. with #ifdef) and then defining appropriate HAVE_... macros.

Second, the HAVE_... macros are used to select the appropriate code path at compile-time. If the feature test is simple or rare, the HAVE_... defines are often omitted.

In Chicken, when writing code that interfaces to C, we can naturally use the same technique inside the C code itself. But what if we want to choose a different Scheme code path based on available system features? For example, say we want to choose between defining the following procedures, depending on whether IPv6-only socket binding is available:

(define (ipv6-v6-only? s) (getsockopt s IPPROTO_IPV6 IPV6_V6ONLY))
(define (ipv6-v6-only? s) (error "IPv6 is not supported"))

In that example, IPPROTO_IPV6 and/or IPV6_V6ONLY may not even be #defined on our system, so we cannot simply create a foreign-variable referring to them, as it could result in a compilation error.

Some possible approaches are:

Feature testing with the feature-test egg proceeds in two phases:

  1. Determine which foreign features are supported by your system by using C header files and the feature-test extension to register features before compile-time.
  2. Alter your generated Scheme code by using cond-expand (effective at compile-time), or by using read syntax from the feature-test-syntax extension (effective at read-time).

Determining feature support

To use feature-test, you

  1. Take any C preprocessor instructions from your main Scheme module and move them into separate include file(s) like myegg.h. Source this file from your module.
  2. Create a Scheme features file such as myegg-features.scm, sourcing the same C header file(s) as your main module (here myegg.h).
  3. Use directives from the feature-test module in myegg-features.scm to select which features you'd like visible to your module.
  4. Compile and execute myegg-features.scm. This will generate a Scheme file on standard output, consisting of one line for each tested feature: either register-feature! if present, or unregister-feature! if absent. Redirect this output to myegg-config.scm.
  5. Compile your main module, adding -X myegg-config.scm to csc. Features will be registered with the compiler before your code is compiled, so they are visible at read-time and compile-time.

feature-test interface

(declare-foreign-features FEATURE1 FEATURE2 ...)syntax

For each feature F, tests whether F is #defined in C, and creates a new boolean #define reflecting this. This new #define is prefixed with the declaration-prefix.

For example, using the (default) declaration prefix HAVE_ and the feature AF_UNIX:

(declaration-prefix HAVE_)
(declare-foreign-features AF_UNIX)
/* generates the C code */
#ifdef AF_UNIX
#define HAVE_AF_UNIX 1
#else
#define HAVE_AF_UNIX 0
#endif

The boolean define HAVE_AF_UNIX is now safely visible to a foreign-variable. In contrast, referring to AF_UNIX from Scheme when undefined would result in a compilation error.

(register-foreign-features FEATURE1 FEATURE2 ...)syntax

For each feature F, accesses the corresponding boolean #define in C, usually generated by declare-foreign-features. Then, generates code to register or unregister the feature for future compiles.

The boolean define is prefixed with the current declaration-prefix, and the registered feature will be prefixed with the registration-prefix.

For example:

(declaration-prefix "HAVE_")
(registration-prefix "MYEGG_")
(register-foreign-features AF_UNIX)

will expand to code like:

(declare-foreign-variable HAVE_AF_UNIX bool "HAVE_AF_UNIX")
(if HAVE_AF_UNIX (emit-register!   'MYEGG_AF_UNIX)
                 (emit-unregister! 'MYEGG_AF_UNIX))

And when compiled and executed, the following is printed to standard output

(register-feature! 'MYEGG_AF_UNIX)     ;; if AF_UNIX was defined
(unregister-feature! 'MYEGG_AF_UNIX)   ;; if AF_UNIX was not defined
(define-foreign-features FEATURE1 FEATURE2 ...)syntax

Equivalent to

(declare-foreign-features FEATURE1 FEATURE2 ...)
(register-foreign-features FEATURE1 FEATURE2 ...)
(declaration-prefix X)syntax

Prefix added to the base feature name when declaring a foreign feature. This can be a string or a symbol.

Defaults to HAVE_.

(registration-prefix X)syntax

Prefix added to the base feature name when registering a foreign feature. This can be a string or a symbol.

Defaults to the empty string.

feature-test example

In this simple and contrived example, we test for the presence of IPPROTO_IPV6 and IPV6_V6ONLY in C, and register or unregister the corresponding features in future compiles. On Windows 2000 and XP, for example, IPPROTO_IPV6 is defined but IPV6_V6ONLY is not.

Our test module mysock.scm allows you to create an IPv6 socket and test its IPv6 bind-only option status; if the option is unavailable, this is detected at compile time and the test is defined to throw a Scheme error. Our little socket6 procedure makes some assumptions of its own, but it's only for illustration.

A real example can be found in the source code to the socket egg.

/* mysock.h */
#ifdef _WIN32
# include <winsock2.h>
# include <ws2tcpip.h>
#else
# include <netinet/in.h>
# include <sys/socket.h>
#endif
;;; mysock-features.scm
#> #include "mysock.h" <#
(use feature-test)
(declaration-prefix HAVE_)   ;; Exact value not important here.
(registration-prefix "")
(define-foreign-features IPPROTO_IPV6 IPV6_V6ONLY)
;;; mysock.setup
(compile mysock-features.scm)
(run (./mysock-features > mysock-config.scm))
(compile -sJ -X mysock-config.scm mysock.scm)
;; [install-extension is omitted for our test]
;;; mysock.meta
;; Can be left blank for our example 
;; but needs to exist even for chicken-install -n.
;;; mysock-config.scm (generated on linux)
(register-feature! 'IPPROTO_IPV6)
(register-feature! 'IPV6_V6ONLY)
;;; mysock-config.scm (generated on mingw32, pre-Vista)
(register-feature! 'IPPROTO_IPV6)
(unregister-feature! 'IPV6_V6ONLY)
;;; mysock.scm
#> #include "mysock.h" <#
(module mysock (ipv6-v6-only? socket6)
 (import scheme chicken foreign)
 ;; Get integer socket option NAME on fd SOCK at LEVEL
 (define getsockopt
  (foreign-lambda* int ((int sock) (int level) (int name))
		   "int ret; socklen_t sz = sizeof(ret);"
		   "if (getsockopt(sock, level, name, (void *)&ret, &sz) < 0)"
		   "  C_return(-1);"
		   "C_return(ret);"))
 ;; Create an IPv6 TCP socket and return its file descriptor
 ;; for testing purposes.  Assume AF_INET6 and SOCK_STREAM are defined.
 (define socket6 (foreign-lambda* int ()
		   "C_return(socket(AF_INET6,SOCK_STREAM,0));"))

 (cond-expand
  ((and IPPROTO_IPV6 IPV6_V6ONLY)
   (define-foreign-variable _ipproto_ipv6 int "IPPROTO_IPV6")
   (define-foreign-variable _ipv6_v6only int "IPV6_V6ONLY")
   (define (ipv6-v6-only? s)
     (getsockopt s _ipproto_ipv6 _ipv6_v6only)))
  (else
   (define (ipv6-v6-only? s)
     (error "IPv6 only binding is not supported")))))
### Build
$ chicken-install -n mysock.setup

### Test on UNIX
$ csi -R mysock -p "(ipv6-v6-only? (socket6))"
0

### Test on Windows XP
> csi -R mysock -p "(ipv6-v6-only? (socket6))"
Error: IPv6 only binding is not supported

Acting on feature support

cond-expand is the usual way to test and act on feature support. However, cond-expand works at macroexpansion time, as does the default #+ reader macro. Therefore, it generally cannot be used inside macros.

To address this, reader macros that do feature testing at read-time are provided in the feature-test-syntax extension. Use it like:

csc -X feature-test-syntax myegg.scm
#+read
#+FEATURE EXPR

Test FEATURE at read-time and, if present, expand to EXPR. FEATURE may be any feature expression permitted in a cond-expand, such as windows or (and windows macosx).

#+ can be used inside macros because it is expanded when the macro form is read, prior to macroexpansion. However, this requires a Chicken version >= 4.6.7, which will omit EXPR if the feature test is false. In earlier versions, the test expands to a (void) form, like the built-in #+. (void) forms are usually illegal inside macro bodies.

Here is an example that assumes Chicken is at least 4.6.7, which will omit EXPR on a false test:

(cond ((eq? x _af_inet) "internet address family")
      #+AF_UNIX
      ((eq? x _af_unix) "unix address family")
      (else "unknown address family")

If AF_UNIX is a registered feature at compile-time, it will be read as:

(cond ((eq? x _af_inet) "internet address family")
      ((eq? x _af_unix) "unix address family")
      (else "unknown address family")

If AF_UNIX is not a registered feature, it will be read as:

(cond ((eq? x _af_inet) "internet address family")
      (else "unknown address family")

However, if AF_UNIX is unregistered and you are using Chicken prior to 4.6.7, it will instead expand into the illegal:

(cond ((eq? x _af_inet) "internet address family")
      (##core#undefined)
      ((eq? x _af_unix) "unix address family")
      (else "unknown address family")

So be careful.

#-read

Like #+, but of opposite polarity.

#?read
#?(FEATURE CONSEQUENT ALTERNATE)

Perform an if-then test at read-time on FEATURE, expanding to CONSEQUENT if FEATURE is present or ALTERNATE if absent. FEATURE may be any feature expression permitted in a cond-expand, such as windows or (and windows macosx).

#? can be used inside macros because it is expanded when the macro form is read, prior to macroexpansion. It expands correctly irrespective of Chicken version.

#? is similar to the Common Lisp idiom

#+FEATURE CONSEQUENT
#-FEATURE ALTERNATE

and, in Chicken versions >= 4.6.7 it is exactly equivalent, even inside macro bodies. However, in previous versions #+ and #- will not work properly inside macros; see #+ for further explanation.

An example of #? which is essentially equivalent to cond-expand:

(define af/unix #?(AF_UNIX _af_unix #f))
;; is basically the same as
(define af/unix (cond-expand (AF_UNIX _af_unix) (else #f)))

A more powerful example of #?:

(cond ((eq? x _af_inet) "internet address family")
      #?(AF_UNIX
         ((eq? x _af_unix) "unix address family")
         (#f))
      (else "unknown address family")

which, if AF_UNIX is a registered feature, expands into

(cond ((eq? x _af_inet) "internet address family")
      ((eq? x _af_unix) "unix address family")
      (else "unknown address family")

and if not, expands into

(cond ((eq? x _af_inet) "internet address family")
      (#f)
      (else "unknown address family")

In the latter case, the false clause cannot succeed and is hopefully optimized out by the compiler. #+ would be more appropriate, but requires Chicken >= 4.6.7. This technique doesn't work with every macro, but you do what you can.

Bugs and limitations

About this egg

Source

https://github.com/ursetto/feature-test-egg

Author

Jim Ursetto

Version history

0.2.0
Chicken 5 support
0.1.1 Internal release-info fix
0.1
Initial release

License

Copyright (c) 2011-2019 Jim Ursetto.  All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer. Redistributions in
  binary form must reproduce the above copyright notice, this list of
  conditions and the following disclaimer in the documentation and/or
  other materials provided with the distribution. Neither the name of the
  author nor the names of its contributors may be used to endorse or
  promote products derived from this software without specific prior
  written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

Contents »