Using Sketchup::Http::Request from C

Anyone had any success using Sketchup::Http::Request to make web requests?

I can type it in the Ruby console but if you invoke it from the C - API, it fails:

VALUE requestclass = rb_const_get(rb_eval_string("Sketchup::Http"), rb_intern("Request"));

VALUE request = rb_funcall(requestclass, rb_intern("new"), 1, rb_str_new2("http://www.example.com/about.html"));

VALUE ret = rb_block_call(request, rb_intern("start"), 0, NULL, my_callback, Qnil);

I haven’t tried it. But with the help of the Gemini AI, (and a lot of corrections to it’s dumb goofs,) here is some test code that might help.

NOTE: There is a corrected file in post 9 below.

http_request.zip (2.5 KB)

#include "ruby.h"

// Helper function to print a C string to the Ruby Console via Kernel#puts
void ruby_puts_cstr(const char *c_str) {
    // 1. Create a new Ruby String VALUE from the C string
    VALUE r_string = rb_str_new_cstr(c_str);
    // 2. Call the Ruby method Kernel#puts with the Ruby String
    rb_funcall(rb_mKernel, rb_intern("puts"), 1, r_string);
}


// The C block function for Hash#each. It receives an array [key, value] as 'yieldarg'.
VALUE header_print_block(VALUE yieldarg, VALUE data, int argc, const VALUE *argv, VALUE passed_block) {
    // Unpack the yielded array [key, value]
    if (TYPE(yieldarg) == T_ARRAY && RARRAY_LEN(yieldarg) >= 2) {
        VALUE key = rb_ary_entry(yieldarg, 0);
        VALUE value = rb_ary_entry(yieldarg, 1);

        // Ensure key and value are string objects before getting the C string pointer.
        StringValue(&key);
        StringValue(&value);

        // Convert key and value to C strings for printing
        char *key_str = RSTRING_PTR(key);
        char *value_str = RSTRING_PTR(value);

        // Format the output string in C and then print via Ruby
        char buffer[256]; 
        snprintf(buffer, sizeof(buffer), "  Header: %s: %s", key_str, value_str);
        
        ruby_puts_cstr(buffer);
    }
    return Qnil;
}


// Helper function to print the status code and headers when a request fails.
// Parameters: status_code (Fixnum VALUE), headers (Hash VALUE)
void print_headers(VALUE status_code, VALUE headers) {
    char status_buffer[128];
    
    // Convert Fixnum VALUE to Ruby String using rb_inspect for printing
    VALUE status_str = rb_inspect(status_code); 

    // Print the status code label on line one
    snprintf(status_buffer, sizeof(status_buffer), "❌ HTTP Failed! Status Code: %s", StringValueCStr(&status_str));
    ruby_puts_cstr(status_buffer);

    // Iterate over the headers Hash using rb_block_call to invoke Hash#each
    rb_block_call(
        headers,                  // The receiver: the headers Hash
        rb_intern("each"),        // The method: "each"
        0,                        // 0 arguments for Hash#each
        NULL,
        header_print_block,       // C function to handle each iteration
        Qnil                      // Custom data (not used here)
    );
}


// The C block function for Sketchup::Http::Request#start.
// It is invoked when the HTTP request completes.
// It expects the yielded value (yieldarg) to be an array [request, response].
VALUE my_callback(VALUE yieldarg, VALUE data, int argc, const VALUE *argv, VALUE passed_block) {

    // Get the persistent module for IVAR access
    VALUE mLightUp = rb_const_get(rb_cObject, rb_intern("LightUp"));
    VALUE mMyExtension = rb_const_get(mLightUp, rb_intern("MyExtension"));

    if (TYPE(yieldarg) == T_ARRAY && RARRAY_LEN(yieldarg) >= 2) {
        VALUE response_obj = rb_ary_entry(yieldarg, 1);
        
        // Get status_code from the response object
        VALUE status_code = rb_funcall(response_obj, rb_intern("status_code"), 0);
        
        // Use FIX2INT for the comparison check
        int status_int = FIX2INT(status_code); 
        char status_buffer[128]; 

        if (status_int >= 200 && status_int < 300) {
            // ✅ Success Branch: Print Status Code and Body Content
            VALUE body = rb_funcall(response_obj, rb_intern("body"), 0);
            
            // 1. Print status message
            VALUE status_str = rb_inspect(status_code);
            snprintf(status_buffer, sizeof(status_buffer), "✅ HTTP Request Succeeded! Status: %s", StringValueCStr(&status_str));
            ruby_puts_cstr(status_buffer);
            
            // 2. Print body length header
            snprintf(status_buffer, sizeof(status_buffer), "--- Response Body (Length: %ld) ---", RSTRING_LEN(body));
            ruby_puts_cstr(status_buffer);
            
            // 3. Print the actual body content directly:
            rb_funcall(rb_mKernel, rb_intern("puts"), 1, body); 

            ruby_puts_cstr("------------------------------------");

        } else {
            // ❌ Failure Branch: Get headers and call print_headers
            VALUE headers = rb_funcall(response_obj, rb_intern("headers"), 0);
            print_headers(status_code, headers);
        }
        
        // Store the response object on the persistent module
        rb_ivar_set(mMyExtension, rb_intern("@response"), response_obj);

    } else {
        rb_warn("Callback did not receive the expected [request, response] array.");
    }

    // Clear the request reference on the persistent module
    rb_ivar_set(mMyExtension, rb_intern("@request"), Qnil);
    return Qnil;
}


// C function used internally by the extension to initiate the request.
// It creates a Sketchup::Http::Request object and calls the asynchronous #start method.
// Parameters: self (the containing Ruby module or Qnil), url_c_str (the C string URL)
VALUE c_make_request(VALUE self, const char *url_c_str) {
    
    // Get the persistent module for IVAR access
    VALUE mLightUp = rb_const_get(rb_cObject, rb_intern("LightUp"));
    VALUE mMyExtension = rb_const_get(mLightUp, rb_intern("MyExtension"));
    
    // Validate that the C string pointer is not NULL
    if (url_c_str == NULL) {
        rb_raise(rb_eArgError, "The URL argument (C string) cannot be NULL.");
    }
    
    // Convert the C string URL to a Ruby String VALUE
    VALUE url_str = rb_str_new_cstr(url_c_str);

    // Get required classes/modules
    VALUE mSketchup = rb_const_get(rb_cObject, rb_intern("Sketchup"));
    VALUE mHttp = rb_const_get(mSketchup, rb_intern("Http"));
    VALUE cRequest = rb_const_get(mHttp, rb_intern("Request"));
    
    // Define the array of arguments for the constructor
    VALUE request_args[] = { url_str };

    // Create a new instance
    VALUE request_obj = rb_class_new_instance(1, request_args, cRequest);
    
    // Store the request object as an instance variable on the persistent module
    rb_ivar_set(mMyExtension, rb_intern("@request"), request_obj);
    
    // Clear the old response variable
    rb_ivar_set(mMyExtension, rb_intern("@response"), Qnil);

    // Call the start method with the C block using rb_block_call
    // rb_block_call(receiver, method_id, argc, argv, func_pointer, data)
    rb_block_call(
        request_obj,       // Receiver: the new request object
        rb_intern("start"),// Method ID: "start"
        0,                 // Arg count for #start (none)
        NULL,              // Arg array (none)
        my_callback,       // The C function to use as the block
        Qnil               // Custom data for the block (not used here)
    );

    // Returns the request object
    return request_obj;
}

// Assume your extension so file is named "MyLightUpExtension.so"
void Init_MyLightUpExtension() {
    // 1. Define the main namespace module: LightUp
    VALUE mLightUp = rb_define_module("LightUp");

    // 2. Define the generic data submodule: LightUp::MyExtension
    // NOTE: Modules can hold instance variables, making them suitable for this purpose.
    VALUE mMyExtension = rb_define_module_under(mLightUp, "MyExtension");
    
    // (Other module definitions, like exposing c_make_request, would go here)
    // or other initializations ....
}

Thanks so much for taking the time Dan.

Unfortunately, it doesn’t work in the same way as my example I posted - as in, it does nothing.

The code pattern is correct - eg if you create an array and invoke #each on it. the callback gets called for each element.

It just appears there is something we’re missing / its actually broken when it comes to Http::Request..

1 Like

This is a good example when being able to see what is happening inside SketchUp’s ruby code would be useful in resolving the problem. How Sketchup makes a socket call is decidedly not the ‘secret sauce’ of Trimble, so not sure why its not accessible.

So, I did not put in puts calls to see what is and isn’t happening.
Are the instance variables getting assigned?
Is it that the callback is just not being called?
Perhaps control needs return to the main application before anything can happen?
SketchUp’s asynchronous features are weird.

OK, TL:DR the docs do warn of premature garbage collection unless request stays in scope and that does appear to be the fundamental problem. I had seen that put presumed because I was invoking the callback block within my C function, the garbage collector would not touch the request reference since it was still in scope. But Http::Request is async so the C stackframe has long gone before it gets processed…

Also the my_callback was examing the yieldarg, it needs to look at argc,argv to get the request,response pair. Annoying that the yieldarg for #each on a dictionary (eg the header_print) IS the key/value pair, but for Http::Request it is not.

All a bit wild west, but think I can beat it into submission now :slight_smile: Cheers!

1 Like

Ah HA! The AI does it again. It actually said two things. The first was that yieldarg would always contain the first parameter, and additional ones would be passed in argc, argv. Then it secondly told me (and put in the example code) that Ruby would bundle them up together into yieldarg as an array. I forgot to prompt the AI further on this discrepancy. Often when I do, it will correct itself or admit it has made a mistake.

Yes, I recognized this and is why I had instance variables of an example extension submodule holding the request and response, (as @request and @response respectively.)

One pattern I was worried about, is that in recent Ruby versions, Ruby wants instance variables initialized before they are ever referenced by a getter. I am kind of sure that this would be done (as above) in the c_make_request function the first time it is called. (Normally, module class and instance variables are initialized, even to nil, when the module is defined.)

So I did report this to Gemini after reopening this exercise conversation. The AI replied …

Adam Billyard is correct. When a method like Sketchup::Http::Request#start yields results to a C block, the parameters are usually passed not via the yieldarg argument, but via the standard C block arguments: argc (argument count) and argv (argument values).

The yieldarg parameter is often reserved for a single value yield, while multiple yielded parameters (like request and response) are passed through the argc/argv array, making it behave like a standard C function call within the block’s context.

So, I then asked why it led me astray with previous explanations …

Takeaway

So, we must be careful and be aware of all 3 patterns when coding C block callback functions.

1 Like

http_request_fixed.zip (2.6 KB)

The revised my_callback function.

// The C block function for Sketchup::Http::Request#start.
// It is invoked when the HTTP request completes.
// Parameters: argc (number of yielded values), argv (array of yielded values)
VALUE my_callback(VALUE yieldarg, VALUE data, int argc, const VALUE *argv, VALUE passed_block) {

    // Get the persistent module for IVAR access
    VALUE mLightUp = rb_const_get(rb_cObject, rb_intern("LightUp"));
    VALUE mMyExtension = rb_const_get(mLightUp, rb_intern("MyExtension"));

    // The block is expected to receive two arguments: [request, response].
    // Check if the expected two arguments are present in argc/argv.
    if (argc == 2) {
        VALUE request_obj = argv[0]; // The Request object is the first argument
        VALUE response_obj = argv[1]; // The Response object is the second argument
        
        // Get status_code from the response object
        VALUE status_code = rb_funcall(response_obj, rb_intern("status_code"), 0);
        
        // Use FIX2INT for the comparison check
        int status_int = FIX2INT(status_code);
        char status_buffer[128];

        if (status_int >= 200 && status_int < 300) {
            // ✅ Success Branch: Print Status Code and Body Content
            VALUE body = rb_funcall(response_obj, rb_intern("body"), 0);
            
            // 1. Print status message
            VALUE status_str = rb_inspect(status_code);
            snprintf(status_buffer, sizeof(status_buffer), "✅ HTTP Request Succeeded! Status: %s", StringValueCStr(&status_str));
            ruby_puts_cstr(status_buffer);
            
            // 2. Print body length header
            snprintf(status_buffer, sizeof(status_buffer), "--- Response Body (Length: %ld) ---", RSTRING_LEN(body));
            ruby_puts_cstr(status_buffer);
            
            // 3. Print the actual body content directly:
            rb_funcall(rb_mKernel, rb_intern("puts"), 1, body);

            ruby_puts_cstr("------------------------------------");

        } else {
            // ❌ Failure Branch: Get headers and call print_headers
            VALUE headers = rb_funcall(response_obj, rb_intern("headers"), 0);
            print_headers(status_code, headers);
        }
        
        // Store the response object on the persistent module
        rb_ivar_set(mMyExtension, rb_intern("@response"), response_obj);

    } else {
        // Handle unexpected argument count
        rb_warn("Callback did not receive the expected two arguments (request, response).");
    }

    // Clear the request reference on the persistent module
    rb_ivar_set(mMyExtension, rb_intern("@request"), Qnil);
    return Qnil;
}

Nice.

Any thoughts of how to wait on the async completing?

The my_callback block function will be called when the response happens.

If you are downloading large data, you can also setup a download progress callback block function.