Is there a C API equivalent for the Ruby API's read_default and write_default?

sketchup

#1

The Ruby API provides methods “read_default” and “write_default”. Are there equivalents in the C API?


#2

Not likely yet, as the SDKs have been written and released (historically) for read and writing SKP files outside the SketchUp application, in either standalone utilities or other graphics applications.

So you’ll hear the team members say things like “we do not support using the C APIs for ‘live’ use.”

And they do not explain to the general public how to hook into the live application.

In the following file, you’ll see the only 2 functions you can get from the SketchUp application:
http://extensions.sketchup.com/developer_center/sketchup_c_api/sketchup/sketchup__info_8h.html

What I’m getting at here is that the settings saved in the registry are application settings specific to each user and not part of the SKP file object model.


Now, all that aside, … what your asking is for C side access to the Windows registry, which can be done using the Microsoft Windows SDK.

https://msdn.microsoft.com/en-us/library/ms724875(v=VS.85).aspx

… but remember that SketchUp formats values it saves (#write_default) into the registry, so when read back (#read_deafult) they can be passed through Ruby’s eval to reproduce the original Ruby objects.


#3

@BruceYoung are you working on a Ruby C extension that runs under SketchUp Ruby by chance ?

If so you can call any Ruby core or SketchUp API method from C.

rb_eval_string("Sketchup.read_default('SomeKey','SomeAttribute')");

… there are also functions: rb_funcall, rb_funcall2, rb_funcall3 and rb_apply.

See the book:

Extending Ruby 1.9: Writing Extensions in C” by Dave Thomas


#4

I have registry read and writes working, and now I have started looking into plist’s for the Mac. Plists are completely different.

Using “rb_eval_string” is a good workaround, but your solution does needs a variable assignment:

rb_eval_string( "RESULT = Sketchup.read_default('section', 'variable', 'default')" );

Additional statements are required to obtain a reference to the Ruby VALUE object “RESULT”, evaluated for Qnil, and finally it will need to be converted into a string.


#5

The drawback to using eval is that the results will not be persistent after the call, unless you use a top level global variable or constant, which needs cleanup afterward.

Perhaps something like this is more to your liking ? (I’m not a C guru so perhaps run this by @thomthom ?)

NOTE: the "by_" prefix means “Bruce Young”. :wink:

#include "ruby.h"

static VALUE by_get_sketchup_default(VALUE dict, VALUE key, VALUE def)
{
    int error;
    VALUE attr;
    
    mSketchup = rb_const_get(rb_cObject, rb_intern("Sketchup"));
    id_read_default = rb_intern("read_default");
    
    VALUE func_args[] = { dict, key, def };
    VALUE prot_args[] = { mSketchup, id_read_default, 3, func_args };
    
    // protect calls to API methods !
    attr = rb_protect((VALUE)rb_funcall2, (VALUE)prot_args, &error);
    
    return error ? Qnil : attr;
}

// use TYPE(attr) to get the type afterward ?

:question:


#6

for v18 some defaults are stored in the .json files, not the .plist…

#read_default will only return a stored string, an empty string, or the given string…

i.e. although the docs state

default (Object) (defaults to: nil) — A default value if the value is not found.

on a mac it should be

default (String) (defaults to: silent error) — A given String if a stored String is not found.

it will not return TrueClass or Numeric class items even when they are stored or given…

john


#7

That’s strange! In the SketchUp 2018 Pro Ruby Console on my Mac I just tried

Sketchup.read_default("foo", "bar")

and it returned nil as documented.
Similarly,

Sketchup.read_default("foo", "bar", "missing")

returned “missing”.

What am I missing?


#8

probably nothing, just my inability to describe the issue that only strings can be retrieved on a mac…

when I used defaults to: silent error I think that it should return an error ‘not a string’ or some message…

many of SU defaults are not stored as strings and cannot be retrieved because of that…

I was under the impression that this was a mac issue and that SU on Windows always stores strings…

Sketchup.read_default("Textures","MaxGLSize") should return 0 or 1…

Sketchup.read_default("Textures", "MaxGLSize").nil? returns ``true` which isn’t the case…

extending your example with a write

Sketchup.write_default("foo", "bar", 0) the value is converted and stored as a string…

how do I explain that better?

john


#9

A json file is purely text, so every value stored there is necessarily converted to some string.

Looking at PrivatePreferences.json, I see

        "Textures": {
            "MaxGLSize": -1
        }

It seems that the -1 gets read back as nil, which makes no sense to me…or is at least an undocumented “feature”.

I also noticed that many values written by SketchUp itself are represented as unquoted strings in the .json, whereas ones written using Ruby Sketchup.write_default are always quoted.

Edit: While poking around at this I came to wonder, is there no way to purge the .json files (or previous .plists or registry) of defaults that are no longer needed - other than manually editing the file/registry? Or do I now have a permanent entry for “foo”:“bar” in my defaults? Seems like a lot of cruft could accumulate, especially if one decides during upgrade that a particular extension is no longer interesting.


#10

On Windows that value is stored as a DWORD which these API methods could never handle.
So it always returns nil in those cases.

These methods can only read string values, but they Ruby eval them so this often causes errors, ie:

Sketchup.read_default("Settings","Shortcut_1").inspect

#=> Error: #<SyntaxError: <main>: syntax error, unexpected tINTEGER, expecting end-of-input
#=> 0 0 0 H selectDollyTool:
#=>    ^>
#=> <main>:1:in `eval'

And there is no way to intercept the error and get the pre eval string value because it seems this is done on the C-side.


Attempting to use these methods to write to non-extension key hives is asking for a corrupt registry and a SketchUp reinstall.

Better to use WIN32OLE or stdlib’s Win32::Registry class (require "win32/registry").


and we’re a bit off the topic here as this is about using the C SDK.


Invalid Signature
#11

I would go with what Dan describe - calling the Ruby methods directly instead of resorting to eval.

A good reference for Ruby C API is this site: https://silverhammermba.github.io/emberb/c/#send

For my own extensions I have created a C++ wrapper for the SketchUp Ruby API. I’ve been wanting to open source it, but I need to refactor some of my proprietary code from it first. The wrapper makes it a whole lot easier to mix the SketchUp Ruby API from a Ruby C extension. Even restore some type-safety as well as enabling the IDE to help out more.


#12

Here is what I have came up with:

rb_eval_string( "module Bhy; BHY_DATA = Sketchup.read_default('Plugin_Bhy_Developer', 'bhy_data' ); end;" );

Note the use of uppercase characters of “BHY_DATA”, resulting in a constant in the module “Bhy” namespace and persistence after the call. (That is my outer module name I’m using in all my Ruby scripting).

The argument string is three Ruby statements. Depending on execution sequence, the statements could be inside a Ruby script instead.
To obtain the data in a ‘C’ extension:

char * szBHY_data_str;
VALUE rb_mBhy = rb_define_module( "Bhy" );
VALUE rbconst_BHY_data = rb_const_get_from( rb_mBhy, rb_intern( "BHY_DATA" ) );
if( NIL_P(rbconst_BHY_data) ) // If read_default is nil, then set text to null string
    szBHY_data_str; = "";
else
    szBHY_data_str; = StringValueCStr( rbconst_BHY_data );

The next thing is to try the above out on my Mac mini (without having to do plists and json files).


#13

This may work now, but Ruby is moving towards not allowing the reassignment of constants.
For example by v2.0 they no longer allow the dynamic reassignment of constants from within methods.
(It raises a ScriptError exception if memory serves.)

So you may be setting yourself up for a “breaking change” down the road.


#14

Why are you jumping through hoops by eval’ing to a constant only to extract the return value from that constant?
Why not call read_default directly?

Also, you check if the value of your constant is nil - then assume the constant contains a string. You will probably crash if it doesn’t. It’s be safer to check if it’s a string first.

Something like this:

VALUE read_default() {
  VALUE mSketchup = rb_const_get(rb_cObject, rb_intern("Sketchup"));
  // TODO: Use rb_protect to protect against Ruby errors. https://silverhammermba.github.io/emberb/c/#rb_protect
  VALUE value = rb_funcall(mSketchup, rb_intern("read_default"), 3,
      GetRubyInterface("my_dictionary"), GetRubyInterface("my_key"), GetRubyInterface("my_default"));
  if (RB_TYPE_P(value, T_STRING)) {
    auto c_string = StringValuePtr(value);
    // TODO: Do stuff to the C String here.
  }
  return value;
}

(And as Dan mentions, don’t reassign constants. You’d be begging for problems in the long run. And it will cause a lot of noise in the console.)


#15

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.